def _setup_versionconfig(known_args, part_configs): try: version_config = VersionConfig( parse=known_args.parse, serialize=known_args.serialize, search=known_args.search, replace=known_args.replace, part_configs=part_configs, ) except sre_constants.error: # TODO: use re.error here mayhaps, also: should we log? sys.exit(1) return version_config
def _load_configuration(config_file, explicit_config, defaults): # setup.cfg supports interpolation - for compatibility we must do the same. if os.path.basename(config_file) == "setup.cfg": config = ConfigParser("") else: config = RawConfigParser("") # don't transform keys to lowercase (which would be the default) config.optionxform = lambda option: option config.add_section("bumpversion") config_file_exists = os.path.exists(config_file) if not config_file_exists: message = "Could not read config file at {}".format(config_file) if explicit_config: raise argparse.ArgumentTypeError(message) logger.info(message) return config, config_file_exists, None, {}, [] logger.info("Reading config file %s:", config_file) with open(config_file, "rt", encoding="utf-8") as config_fp: config_content = config_fp.read() config_newlines = config_fp.newlines # TODO: this is a DEBUG level log logger.info(config_content) config.read_string(config_content) log_config = io.StringIO() config.write(log_config) if config.has_option("bumpversion", "files"): warnings.warn( "'files =' configuration will be deprecated, please use [bumpversion:file:...]", PendingDeprecationWarning, ) defaults.update(dict(config.items("bumpversion"))) for listvaluename in ("serialize", ): try: value = config.get("bumpversion", listvaluename) defaults[listvaluename] = list( filter(None, (x.strip() for x in value.splitlines()))) except NoOptionError: pass # no default value then ;) for boolvaluename in ("commit", "tag", "dry_run"): try: defaults[boolvaluename] = config.getboolean( "bumpversion", boolvaluename) except NoOptionError: pass # no default value then ;) part_configs = {} files = [] for section_name in config.sections(): section_type_match = RE_DETECT_SECTION_TYPE.match(section_name) if not section_type_match: continue section_type = section_type_match.groupdict() section_value = section_type.get("value") section_config = dict(config.items(section_name)) if section_type.get("part"): ThisVersionPartConfiguration = NumericVersionPartConfiguration if "values" in section_config: section_config["values"] = list( filter( None, (x.strip() for x in section_config["values"].splitlines()), )) ThisVersionPartConfiguration = ConfiguredVersionPartConfiguration part_configs[section_value] = ThisVersionPartConfiguration( **section_config) elif section_type.get("file"): filename = section_value if "serialize" in section_config: section_config["serialize"] = list( filter( None, (x.strip().replace("\\n", "\n") for x in section_config["serialize"].splitlines()), )) section_config["part_configs"] = part_configs if "parse" not in section_config: section_config["parse"] = defaults.get( "parse", r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)") if "serialize" not in section_config: section_config["serialize"] = defaults.get( "serialize", ["{major}.{minor}.{patch}"]) if "search" not in section_config: section_config["search"] = defaults.get( "search", "{current_version}") if "replace" not in section_config: section_config["replace"] = defaults.get( "replace", "{new_version}") version_config = VersionConfig(**section_config) if section_type.get("file") == "glob": for filename_glob in glob.glob(filename, recursive=True): files.append(ConfiguredFile(filename_glob, version_config)) else: files.append(ConfiguredFile(filename, version_config)) return config, config_file_exists, config_newlines, part_configs, files
def main(original_args=None): positionals, args = split_args_in_optional_and_positional( sys.argv[1:] if original_args is None else original_args ) if len(positionals[1:]) > 2: warnings.warn( "Giving multiple files on the command line will be deprecated, please use [bumpversion:file:...] in a config file.", PendingDeprecationWarning, ) parser1 = argparse.ArgumentParser(add_help=False) parser1.add_argument( "--config-file", metavar="FILE", default=argparse.SUPPRESS, required=False, help="Config file to read most of the variables from (default: .bumpversion.cfg)", ) parser1.add_argument( "--verbose", action="count", default=0, help="Print verbose logging to stderr", required=False, ) parser1.add_argument( "--list", action="store_true", default=False, help="List machine readable information", required=False, ) parser1.add_argument( "--allow-dirty", action="store_true", default=False, help="Don't abort if working directory is dirty", required=False, ) known_args, remaining_argv = parser1.parse_known_args(args) logformatter = logging.Formatter("%(message)s") if len(logger_list.handlers) == 0: ch2 = logging.StreamHandler(sys.stdout) ch2.setFormatter(logformatter) logger_list.addHandler(ch2) if known_args.list: logger_list.setLevel(logging.DEBUG) try: log_level = [logging.WARNING, logging.INFO, logging.DEBUG][known_args.verbose] except IndexError: log_level = logging.DEBUG root_logger = logging.getLogger('') root_logger.setLevel(log_level) logger.debug("Starting {}".format(DESCRIPTION)) defaults = {} vcs_info = {} for vcs in VCS: if vcs.is_usable(): vcs_info.update(vcs.latest_tag_info()) if "current_version" in vcs_info: defaults["current_version"] = vcs_info["current_version"] explicit_config = hasattr(known_args, "config_file") if explicit_config: config_file = known_args.config_file elif not os.path.exists(".bumpversion.cfg") and os.path.exists("setup.cfg"): config_file = "setup.cfg" else: config_file = ".bumpversion.cfg" # setup.cfg supports interpolation - for compatibility we must do the same. if os.path.basename(config_file) == "setup.cfg": config = ConfigParser("") else: config = RawConfigParser("") # don't transform keys to lowercase (which would be the default) config.optionxform = lambda option: option config.add_section("bumpversion") config_file_exists = os.path.exists(config_file) part_configs = {} files = [] if config_file_exists: logger.info("Reading config file {}:".format(config_file)) # TODO: this is a DEBUG level log with io.open(config_file, "rt", encoding="utf-8") as f: logger.info(f.read()) config_new_lines = f.newlines try: # TODO: we're reading the config file twice. config.read_file(io.open(config_file, "rt", encoding="utf-8")) except AttributeError: # python 2 standard ConfigParser doesn't have read_file, # only deprecated readfp config.readfp(io.open(config_file, "rt", encoding="utf-8")) log_config = StringIO() config.write(log_config) if "files" in dict(config.items("bumpversion")): warnings.warn( "'files =' configuration will be deprecated, please use [bumpversion:file:...]", PendingDeprecationWarning, ) defaults.update(dict(config.items("bumpversion"))) for listvaluename in ("serialize",): try: value = config.get("bumpversion", listvaluename) defaults[listvaluename] = list( filter(None, (x.strip() for x in value.splitlines())) ) except NoOptionError: pass # no default value then ;) for boolvaluename in ("commit", "tag", "dry_run"): try: defaults[boolvaluename] = config.getboolean( "bumpversion", boolvaluename ) except NoOptionError: pass # no default value then ;) for section_name in config.sections(): section_name_match = re.compile("^bumpversion:(file|part):(.+)").match( section_name ) if not section_name_match: continue section_prefix, section_value = section_name_match.groups() section_config = dict(config.items(section_name)) if section_prefix == "part": ThisVersionPartConfiguration = NumericVersionPartConfiguration if "values" in section_config: section_config["values"] = list( filter( None, (x.strip() for x in section_config["values"].splitlines()), ) ) ThisVersionPartConfiguration = ConfiguredVersionPartConfiguration part_configs[section_value] = ThisVersionPartConfiguration( **section_config ) elif section_prefix == "file": filename = section_value if "serialize" in section_config: section_config["serialize"] = list( filter( None, ( x.strip().replace("\\n", "\n") for x in section_config["serialize"].splitlines() ), ) ) section_config["part_configs"] = part_configs if "parse" not in section_config: section_config["parse"] = defaults.get( "parse", r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)" ) if "serialize" not in section_config: section_config["serialize"] = defaults.get( "serialize", [str("{major}.{minor}.{patch}")] ) if "search" not in section_config: section_config["search"] = defaults.get( "search", "{current_version}" ) if "replace" not in section_config: section_config["replace"] = defaults.get("replace", "{new_version}") files.append(ConfiguredFile(filename, VersionConfig(**section_config))) else: message = "Could not read config file at {}".format(config_file) if explicit_config: raise argparse.ArgumentTypeError(message) logger.info(message) parser2 = argparse.ArgumentParser( prog="bumpversion", add_help=False, parents=[parser1] ) parser2.set_defaults(**defaults) parser2.add_argument( "--current-version", metavar="VERSION", help="Version that needs to be updated", required=False, ) parser2.add_argument( "--parse", metavar="REGEX", help="Regex parsing the version string", default=defaults.get( "parse", r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)" ), ) parser2.add_argument( "--serialize", metavar="FORMAT", action=DiscardDefaultIfSpecifiedAppendAction, help="How to format what is parsed back to a version", default=defaults.get("serialize", [str("{major}.{minor}.{patch}")]), ) parser2.add_argument( "--search", metavar="SEARCH", help="Template for complete string to search", default=defaults.get("search", "{current_version}"), ) parser2.add_argument( "--replace", metavar="REPLACE", help="Template for complete string to replace", default=defaults.get("replace", "{new_version}"), ) known_args, remaining_argv = parser2.parse_known_args(args) defaults.update(vars(known_args)) assert isinstance(known_args.serialize, list), "Argument `serialize` must be a list" context = dict( list(time_context.items()) + list(prefixed_environ().items()) + list(vcs_info.items()) ) try: vc = VersionConfig( parse=known_args.parse, serialize=known_args.serialize, search=known_args.search, replace=known_args.replace, part_configs=part_configs, ) except sre_constants.error as e: # TODO: use re.error here mayhaps, also: should we log? sys.exit(1) current_version = ( vc.parse(known_args.current_version) if known_args.current_version else None ) new_version = None if "new_version" not in defaults and known_args.current_version: try: if current_version and len(positionals) > 0: logger.info("Attempting to increment part '{}'".format(positionals[0])) new_version = current_version.bump(positionals[0], vc.order()) logger.info("Values are now: " + keyvaluestring(new_version._values)) defaults["new_version"] = vc.serialize(new_version, context) except MissingValueForSerializationException as e: logger.info("Opportunistic finding of new_version failed: " + e.message) except IncompleteVersionRepresentationException as e: logger.info("Opportunistic finding of new_version failed: " + e.message) except KeyError as e: logger.info("Opportunistic finding of new_version failed") parser3 = argparse.ArgumentParser( prog="bumpversion", description=DESCRIPTION, formatter_class=argparse.ArgumentDefaultsHelpFormatter, conflict_handler="resolve", parents=[parser2], ) parser3.set_defaults(**defaults) parser3.add_argument( "--current-version", metavar="VERSION", help="Version that needs to be updated", required="current_version" not in defaults, ) parser3.add_argument( "--dry-run", "-n", action="store_true", default=False, help="Don't write any files, just pretend.", ) parser3.add_argument( "--new-version", metavar="VERSION", help="New version that should be in the files", required="new_version" not in defaults, ) commitgroup = parser3.add_mutually_exclusive_group() commitgroup.add_argument( "--commit", action="store_true", dest="commit", help="Commit to version control", default=defaults.get("commit", False), ) commitgroup.add_argument( "--no-commit", action="store_false", dest="commit", help="Do not commit to version control", default=argparse.SUPPRESS, ) taggroup = parser3.add_mutually_exclusive_group() taggroup.add_argument( "--tag", action="store_true", dest="tag", default=defaults.get("tag", False), help="Create a tag in version control", ) taggroup.add_argument( "--no-tag", action="store_false", dest="tag", help="Do not create a tag in version control", default=argparse.SUPPRESS, ) signtagsgroup = parser3.add_mutually_exclusive_group() signtagsgroup.add_argument( "--sign-tags", action="store_true", dest="sign_tags", help="Sign tags if created", default=defaults.get("sign_tags", False), ) signtagsgroup.add_argument( "--no-sign-tags", action="store_false", dest="sign_tags", help="Do not sign tags if created", default=argparse.SUPPRESS, ) parser3.add_argument( "--tag-name", metavar="TAG_NAME", help="Tag name (only works with --tag)", default=defaults.get("tag_name", "v{new_version}"), ) parser3.add_argument( "--tag-message", metavar="TAG_MESSAGE", dest="tag_message", help="Tag message", default=defaults.get( "tag_message", "Bump version: {current_version} → {new_version}" ), ) parser3.add_argument( "--message", "-m", metavar="COMMIT_MSG", help="Commit message", default=defaults.get( "message", "Bump version: {current_version} → {new_version}" ), ) file_names = [] if "files" in defaults: assert defaults["files"] is not None file_names = defaults["files"].split(" ") parser3.add_argument("part", help="Part of the version to be bumped.") parser3.add_argument( "files", metavar="file", nargs="*", help="Files to change", default=file_names ) args = parser3.parse_args(remaining_argv + positionals) if args.dry_run: logger.info("Dry run active, won't touch any files.") if args.new_version: new_version = vc.parse(args.new_version) logger.info("New version will be '{}'".format(args.new_version)) file_names = file_names or positionals[1:] for file_name in file_names: files.append(ConfiguredFile(file_name, vc)) for vcs in VCS: if vcs.is_usable(): try: vcs.assert_nondirty() except WorkingDirectoryIsDirtyException as e: if not defaults["allow_dirty"]: logger.warning( "{}\n\nUse --allow-dirty to override this if you know what you're doing.".format( e.message ) ) raise break else: vcs = None # make sure files exist and contain version string logger.info( "Asserting files {} contain the version string:".format( ", ".join([str(f) for f in files]) ) ) for f in files: f.should_contain_version(current_version, context) # change version string in files for f in files: f.replace(current_version, new_version, context, args.dry_run) commit_files = [f.path for f in files] config.set("bumpversion", "new_version", args.new_version) for key, value in config.items("bumpversion"): logger_list.info("{}={}".format(key, value)) config.remove_option("bumpversion", "new_version") config.set("bumpversion", "current_version", args.new_version) new_config = StringIO() try: write_to_config_file = (not args.dry_run) and config_file_exists logger.info( "{} to config file {}:".format( "Would write" if not write_to_config_file else "Writing", config_file ) ) config.write(new_config) logger.info(new_config.getvalue()) if write_to_config_file: with io.open(config_file, "wt", encoding="utf-8", newline=config_new_lines) as f: f.write(new_config.getvalue()) except UnicodeEncodeError: warnings.warn( "Unable to write UTF-8 to config file, because of an old configparser version. " "Update with `pip install --upgrade configparser`." ) if config_file_exists: commit_files.append(config_file) if not vcs: return assert vcs.is_usable(), "Did find '{}' unusable, unable to commit.".format( vcs.__name__ ) do_commit = args.commit and not args.dry_run do_tag = args.tag and not args.dry_run logger.info( "{} {} commit".format( "Would prepare" if not do_commit else "Preparing", vcs.__name__ ) ) for path in commit_files: logger.info( "{} changes in file '{}' to {}".format( "Would add" if not do_commit else "Adding", path, vcs.__name__ ) ) if do_commit: vcs.add_path(path) vcs_context = { "current_version": args.current_version, "new_version": args.new_version, } vcs_context.update(time_context) vcs_context.update(prefixed_environ()) commit_message = args.message.format(**vcs_context) logger.info( "{} to {} with message '{}'".format( "Would commit" if not do_commit else "Committing", vcs.__name__, commit_message, ) ) if do_commit: vcs.commit(message=commit_message) sign_tags = args.sign_tags tag_name = args.tag_name.format(**vcs_context) tag_message = args.tag_message.format(**vcs_context) logger.info( "{} '{}' {} in {} and {}".format( "Would tag" if not do_tag else "Tagging", tag_name, "with message '{}'".format(tag_message) if tag_message else "without message", vcs.__name__, "signing" if sign_tags else "not signing", ) ) if do_tag: vcs.tag(sign_tags, tag_name, tag_message)