# -*- coding: utf-8 -*-
"""
Functionality to update one PyScaffold version to another
"""
import os
from functools import reduce
from os.path import exists as path_exists
from os.path import join as join_path
from pkg_resources import parse_version
from . import __version__ as pyscaffold_version
from .contrib.configupdater import ConfigUpdater
from .log import logger
from .structure import FileOp
from .utils import get_id, get_setup_requires_version
[docs]def apply_update_rules(struct, opts, prefix=None):
"""Apply update rules using :obj:`~.FileOp` to a directory structure.
As a result the filtered structure keeps only the files that actually will
be written.
Args:
opts (dict): options of the project, containing the following flags:
- **update**: when the project already exists and should be updated
- **force**: overwrite all the files that already exist
struct (dict): directory structure as dictionary of dictionaries
(in this tree representation, each leaf can be just a
string or a tuple also containing an update rule)
prefix (str): prefix path for the structure
Returns:
tuple(dict, dict):
directory structure with keys removed according to the rules
(in this tree representation, all the leaves are strings) and input
options
"""
if prefix is None:
prefix = os.getcwd()
filtered = {}
for k, v in struct.items():
if isinstance(v, dict):
v, _ = apply_update_rules(v, opts, join_path(prefix, k))
else:
path = join_path(prefix, k)
v = apply_update_rule_to_file(path, v, opts)
if v is not None:
filtered[k] = v
return filtered, opts
[docs]def apply_update_rule_to_file(path, value, opts):
"""Applies the update rule to a given file path
Args:
path (str): file path
value (tuple or str): content (and update rule)
opts (dict): options of the project, containing the following flags:
- **update**: if the project already exists and should be updated
- **force**: overwrite all the files that already exist
Returns:
content of the file if it should be generated or None otherwise.
"""
if isinstance(value, (tuple, list)):
content, rule = value
else:
content, rule = value, None
update = opts.get("update")
force = opts.get("force")
skip = (
update
and not force
and (
rule == FileOp.NO_CREATE
or path_exists(path)
and rule == FileOp.NO_OVERWRITE
)
)
if skip:
logger.report("skip", path)
return None
return content
[docs]def read_setupcfg(project_path):
"""Reads-in setup.cfg for updating
Args:
project_path (str): path to project
Returns:
"""
path = join_path(project_path, "setup.cfg")
updater = ConfigUpdater()
updater.read(path, encoding="utf-8")
return updater
[docs]def invoke_action(action, struct, opts):
"""Invoke action with proper logging.
Args:
struct (dict): project representation as (possibly) nested
:obj:`dict`.
opts (dict): given options, see :obj:`create_project` for
an extensive list.
Returns:
tuple(dict, dict): updated project representation and options
"""
logger.report("invoke", get_id(action))
with logger.indent():
struct, opts = action(struct, opts)
return struct, opts
[docs]def get_curr_version(project_path):
"""Retrieves the PyScaffold version that put up the scaffold
Args:
project_path: path to project
Returns:
Version: version specifier
"""
setupcfg = read_setupcfg(project_path).to_dict()
return parse_version(setupcfg["pyscaffold"]["version"])
[docs]def version_migration(struct, opts):
"""Migrations from one version to another
Args:
struct (dict): previous directory structure (ignored)
opts (dict): options of the project
Returns:
tuple(dict, dict):
structure as dictionary of dictionaries and input options
"""
update = opts.get("update")
if not update:
return struct, opts
curr_version = get_curr_version(opts["project"])
# specify how to migrate from one version to another as ordered list
migration_plans = [(parse_version("3.1"), [add_entrypoints, add_setup_requires])]
for plan_version, plan_actions in migration_plans:
if curr_version < plan_version:
struct, opts = reduce(
lambda acc, f: invoke_action(f, *acc), plan_actions, (struct, opts)
)
# note the updating version in setup.cfg for future use
update_pyscaffold_version(opts["project"], opts["pretend"])
# replace the old version with the updated one
opts["version"] = pyscaffold_version
return struct, opts
[docs]def add_entrypoints(struct, opts):
"""Add [options.entry_points] to setup.cfg
Args:
struct (dict): previous directory structure (ignored)
opts (dict): options of the project
Returns:
tuple(dict, dict):
structure as dictionary of dictionaries and input options
"""
setupcfg = read_setupcfg(opts["project"])
section_str = """[options.entry_points]
# Add here console scripts like:
# console_scripts =
# script_name = ${package}.module:function
# For example:
# console_scripts =
# fibonacci = ${package}.skeleton:run
# And any other entry points, for example:
# pyscaffold.cli =
# awesome = pyscaffoldext.awesome.extension:AwesomeExtension
"""
new_section_name = "options.entry_points"
if new_section_name in setupcfg:
return struct, opts
new_section = ConfigUpdater()
new_section.read_string(section_str)
new_section = new_section[new_section_name]
add_after_sect = "options.extras_require"
if add_after_sect not in setupcfg:
# user removed it for some reason, default to metadata
add_after_sect = "metadata"
setupcfg[add_after_sect].add_after.section(new_section).space()
if not opts["pretend"]:
setupcfg.update_file()
return struct, opts
[docs]def add_setup_requires(struct, opts):
"""Add `setup_requires` in setup.cfg
Args:
struct (dict): previous directory structure (ignored)
opts (dict): options of the project
Returns:
tuple(dict, dict):
structure as dictionary of dictionaries and input options
"""
setupcfg = read_setupcfg(opts["project"])
comment = "# DON'T CHANGE THE FOLLOWING LINE! " "IT WILL BE UPDATED BY PYSCAFFOLD!"
options = setupcfg["options"]
if "setup_requires" in options:
return struct, opts
version_str = get_setup_requires_version()
(
options["package_dir"]
.add_after.comment(comment)
.option("setup_requires", version_str)
)
if not opts["pretend"]:
setupcfg.update_file()
return struct, opts
[docs]def update_pyscaffold_version(project_path, pretend):
"""Update `setup_requires` in setup.cfg
Args:
project_path (str): path to project
pretend (bool): only pretend to do something
"""
setupcfg = read_setupcfg(project_path)
setupcfg["options"]["setup_requires"] = get_setup_requires_version()
setupcfg["pyscaffold"]["version"] = pyscaffold_version
if not pretend:
setupcfg.update_file()