From: Mike Werezak Date: Sat, 26 Apr 2025 18:57:24 +0000 (-0400) Subject: Add file_path_separator to apply to all path splitting X-Git-Tag: rel_1_16_0~17^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=9913fdb8e20262ccbc34f715b044d36b4be8b5ad;p=thirdparty%2Fsqlalchemy%2Falembic.git Add file_path_separator to apply to all path splitting Added new option to the ConfigParser config (typically via ``alembic.ini``) ``path_separator``. This new option supersedes the previous similar option ``version_path_separator``. The new ``path_separator`` option applies to the path splitting mechanism of both the ``version_locations`` option as well as the ``prepend_sys_path`` option, and in newly rendered ``alembic.ini`` files will use the value ``os``, which means to use the operating system path separator when splitting these string values into a list of paths. The new attribute applies necessary os-dependent path splitting to the ``prepend_sys_path`` option so that windows paths which contain drive letters with colons are not inadvertently split, whereas previously os-dependent path splitting were only available for the ``version_locations`` option. Existing installations that do not have ``path_separator`` present will continue to make use of ``version_path_separator`` when parsing the ``version_locations`` option, or splitting on spaces / commas if ``version_path_separator`` is also not present. ``prepend_sys_path`` will continue to be split on spaces/commas/colons if ``path_separator`` is not present. Under all of these fallback conditions, a deprecation warning is now emitted encouraging to set ``path_separator``. Pull request courtesy Mike Werezak. This change also begins to move some of the role of interpreting of specific config options into the Config object. This process will continue as we look to add toml support to config. Co-authored-by: Mike Bayer Fixes: #1330 Closes: #1331 Pull-request: https://github.com/sqlalchemy/alembic/pull/1331 Pull-request-sha: f16b35d69acddd046cbba8f28e46219c872c151d Change-Id: Idef47e8a6947210f1eb63c3d16c4be553effa3a2 --- diff --git a/alembic/__init__.py b/alembic/__init__.py index 1e751e9b..16270d14 100644 --- a/alembic/__init__.py +++ b/alembic/__init__.py @@ -1,4 +1,4 @@ from . import context from . import op -__version__ = "1.15.3" +__version__ = "1.16.0" diff --git a/alembic/config.py b/alembic/config.py index 18ab0f16..dc7d3f81 100644 --- a/alembic/config.py +++ b/alembic/config.py @@ -5,6 +5,7 @@ from argparse import Namespace from configparser import ConfigParser import inspect import os +import re import sys from typing import Any from typing import cast @@ -340,6 +341,119 @@ class Config: ), ) + def _get_file_separator_char(self, *names: str) -> Optional[str]: + for name in names: + separator = self.get_main_option(name) + if separator is not None: + break + else: + return None + + split_on_path = { + "space": " ", + "newline": "\n", + "os": os.pathsep, + ":": ":", + ";": ";", + } + + try: + sep = split_on_path[separator] + except KeyError as ke: + raise ValueError( + "'%s' is not a valid value for %s; " + "expected 'space', 'newline', 'os', ':', ';'" + % (separator, name) + ) from ke + else: + if name == "version_path_separator": + util.warn_deprecated( + "The version_path_separator configuration parameter " + "is deprecated; please use path_separator" + ) + return sep + + def get_version_locations_list(self) -> Optional[list[str]]: + + version_locations_str = self.get_main_option("version_locations") + + if version_locations_str: + split_char = self._get_file_separator_char( + "path_separator", "version_path_separator" + ) + + if split_char is None: + + # legacy behaviour for backwards compatibility + util.warn_deprecated( + "No path_separator found in configuration; " + "falling back to legacy splitting on spaces/commas " + "for version_locations. Consider adding " + "path_separator=os to Alembic config." + ) + + _split_on_space_comma = re.compile(r", *|(?: +)") + return _split_on_space_comma.split(version_locations_str) + else: + return [ + x.strip() + for x in version_locations_str.split(split_char) + if x + ] + else: + return None + + def get_prepend_sys_paths_list(self) -> Optional[list[str]]: + prepend_sys_path_str = self.get_main_option("prepend_sys_path") + + if prepend_sys_path_str: + split_char = self._get_file_separator_char("path_separator") + + if split_char is None: + + # legacy behaviour for backwards compatibility + util.warn_deprecated( + "No path_separator found in configuration; " + "falling back to legacy splitting on spaces, commas, " + "and colons for prepend_sys_path. Consider adding " + "path_separator=os to Alembic config." + ) + + _split_on_space_comma_colon = re.compile(r", *|(?: +)|\:") + return _split_on_space_comma_colon.split(prepend_sys_path_str) + else: + return [ + x.strip() + for x in prepend_sys_path_str.split(split_char) + if x + ] + else: + return None + + def get_hooks_list(self) -> list[PostWriteHookConfig]: + _split_on_space_comma = re.compile(r", *|(?: +)") + + hook_config = self.get_section("post_write_hooks", {}) + names = _split_on_space_comma.split(hook_config.get("hooks", "")) + + hooks: list[PostWriteHookConfig] = [] + for name in names: + if not name: + continue + opts = { + key[len(name) + 1 :]: hook_config[key] + for key in hook_config + if key.startswith(name + ".") + } + + opts["_hook_name"] = name + hooks.append(opts) + + return hooks + + +PostWriteHookConfig = Mapping[str, str] + class MessagingOptions(TypedDict, total=False): quiet: bool diff --git a/alembic/script/base.py b/alembic/script/base.py index 30df6ddb..3fa3c282 100644 --- a/alembic/script/base.py +++ b/alembic/script/base.py @@ -11,7 +11,6 @@ from typing import Any from typing import cast from typing import Iterator from typing import List -from typing import Mapping from typing import Optional from typing import Sequence from typing import Set @@ -32,6 +31,7 @@ if TYPE_CHECKING: from .revision import Revision from ..config import Config from ..config import MessagingOptions + from ..config import PostWriteHookConfig from ..runtime.migration import RevisionStep from ..runtime.migration import StampStep @@ -50,9 +50,6 @@ _only_source_rev_file = re.compile(r"(?!\.\#|__init__)(.*\.py)$") _legacy_rev = re.compile(r"([a-f0-9]+)\.py$") _slug_re = re.compile(r"\w+") _default_file_template = "%(rev)s_%(slug)s" -_split_on_space_comma = re.compile(r", *|(?: +)") - -_split_on_space_comma_colon = re.compile(r", *|(?: +)|\:") class ScriptDirectory: @@ -84,7 +81,7 @@ class ScriptDirectory: sourceless: bool = False, output_encoding: str = "utf-8", timezone: Optional[str] = None, - hook_config: Optional[Mapping[str, str]] = None, + hooks: list[PostWriteHookConfig] = [], recursive_version_locations: bool = False, messaging_opts: MessagingOptions = cast( "MessagingOptions", util.EMPTY_DICT @@ -98,7 +95,7 @@ class ScriptDirectory: self.output_encoding = output_encoding self.revision_map = revision.RevisionMap(self._load_revisions) self.timezone = timezone - self.hook_config = hook_config + self.hooks = hooks self.recursive_version_locations = recursive_version_locations self.messaging_opts = messaging_opts @@ -168,7 +165,7 @@ class ScriptDirectory: script_location = config.get_main_option("script_location") if script_location is None: raise util.CommandError( - "No 'script_location' key " "found in configuration." + "No 'script_location' key found in configuration." ) truncate_slug_length: Optional[int] tsl = config.get_main_option("truncate_slug_length") @@ -177,53 +174,9 @@ class ScriptDirectory: else: truncate_slug_length = None - version_locations_str = config.get_main_option("version_locations") - version_locations: Optional[List[str]] - if version_locations_str: - version_path_separator = config.get_main_option( - "version_path_separator" - ) - - split_on_path = { - None: None, - "space": " ", - "newline": "\n", - "os": os.pathsep, - ":": ":", - ";": ";", - } - - try: - split_char: Optional[str] = split_on_path[ - version_path_separator - ] - except KeyError as ke: - raise ValueError( - "'%s' is not a valid value for " - "version_path_separator; " - "expected 'space', 'newline', 'os', ':', ';'" - % version_path_separator - ) from ke - else: - if split_char is None: - # legacy behaviour for backwards compatibility - version_locations = _split_on_space_comma.split( - version_locations_str - ) - else: - version_locations = [ - x.strip() - for x in version_locations_str.split(split_char) - if x - ] - else: - version_locations = None - - prepend_sys_path = config.get_main_option("prepend_sys_path") + prepend_sys_path = config.get_prepend_sys_paths_list() if prepend_sys_path: - sys.path[:0] = list( - _split_on_space_comma_colon.split(prepend_sys_path) - ) + sys.path[:0] = prepend_sys_path rvl = config.get_main_option("recursive_version_locations") == "true" return ScriptDirectory( @@ -234,9 +187,9 @@ class ScriptDirectory: truncate_slug_length=truncate_slug_length, sourceless=config.get_main_option("sourceless") == "true", output_encoding=config.get_main_option("output_encoding", "utf-8"), - version_locations=version_locations, + version_locations=config.get_version_locations_list(), timezone=config.get_main_option("timezone"), - hook_config=config.get_section("post_write_hooks", {}), + hooks=config.get_hooks_list(), recursive_version_locations=rvl, messaging_opts=config.messaging_opts, ) @@ -763,7 +716,7 @@ class ScriptDirectory: **kw, ) - post_write_hooks = self.hook_config + post_write_hooks = self.hooks if post_write_hooks: write_hooks._run_hooks(path, post_write_hooks) diff --git a/alembic/script/write_hooks.py b/alembic/script/write_hooks.py index 99771479..64bd3873 100644 --- a/alembic/script/write_hooks.py +++ b/alembic/script/write_hooks.py @@ -10,13 +10,14 @@ from typing import Any from typing import Callable from typing import Dict from typing import List -from typing import Mapping from typing import Optional -from typing import Union +from typing import TYPE_CHECKING from .. import util from ..util import compat +if TYPE_CHECKING: + from ..config import PostWriteHookConfig REVISION_SCRIPT_TOKEN = "REVISION_SCRIPT_FILENAME" @@ -42,9 +43,7 @@ def register(name: str) -> Callable: return decorate -def _invoke( - name: str, revision: str, options: Mapping[str, Union[str, int]] -) -> Any: +def _invoke(name: str, revision: str, options: PostWriteHookConfig) -> Any: """Invokes the formatter registered for the given name. :param name: The name of a formatter in the registry @@ -63,24 +62,13 @@ def _invoke( return hook(revision, options) -def _run_hooks(path: str, hook_config: Mapping[str, str]) -> None: +def _run_hooks(path: str, hooks: list[PostWriteHookConfig]) -> None: """Invoke hooks for a generated revision.""" - from .base import _split_on_space_comma - - names = _split_on_space_comma.split(hook_config.get("hooks", "")) - - for name in names: - if not name: - continue - opts = { - key[len(name) + 1 :]: hook_config[key] - for key in hook_config - if key.startswith(name + ".") - } - opts["_hook_name"] = name + for hook in hooks: + name = hook["_hook_name"] try: - type_ = opts["type"] + type_ = hook["type"] except KeyError as ke: raise util.CommandError( f"Key {name}.type is required for post write hook {name!r}" @@ -89,7 +77,7 @@ def _run_hooks(path: str, hook_config: Mapping[str, str]) -> None: with util.status( f"Running post write hook {name!r}", newline=True ): - _invoke(type_, path, opts) + _invoke(type_, path, hook) def _parse_cmdline_options(cmdline_options_str: str, path: str) -> List[str]: diff --git a/alembic/templates/async/alembic.ini.mako b/alembic/templates/async/alembic.ini.mako index 7ffd7926..1eb347c0 100644 --- a/alembic/templates/async/alembic.ini.mako +++ b/alembic/templates/async/alembic.ini.mako @@ -10,7 +10,8 @@ script_location = ${script_location} # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s # sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. +# defaults to the current working directory. for multiple paths, the path separator +# is defined by "path_separator" below. prepend_sys_path = . # timezone to use when rendering the date within the migration file @@ -36,21 +37,36 @@ prepend_sys_path = . # version location specification; This defaults # to ${script_location}/versions. When using multiple version # directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" below. +# The path separator used here should be the separator specified by "path_separator" +# below. # version_locations = %(here)s/bar:%(here)s/bat:${script_location}/versions -# version path separator; As mentioned above, this is the character used to split -# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. -# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. -# Valid values for version_path_separator are: +# path_separator; This indicates what character is used to split lists of file +# paths, including version_locations and prepend_sys_path within configparser +# files such as alembic.ini. +# The default rendered in new alembic.ini files is "os", which uses os.pathsep +# to provide os-dependent path splitting. # -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -# version_path_separator = newline +# Note that in order to support legacy alembic.ini files, this default does NOT +# take place if path_separator is not present in alembic.ini. If this +# option is omitted entirely, fallback logic is as follows: +# +# 1. Parsing of the version_locations option falls back to using the legacy +# "version_path_separator" key, which if absent then falls back to the legacy +# behavior of splitting on spaces and/or commas. +# 2. Parsing of the prepend_sys_path option falls back to the legacy +# behavior of splitting on spaces, commas, or colons. +# +# Valid values for path_separator are: +# +# path_separator = : +# path_separator = ; +# path_separator = space +# path_separator = newline # # Use os.pathsep. Default configuration used for new projects. -version_path_separator = os +path_separator = os + # set to 'true' to search source files recursively # in each "version_locations" directory diff --git a/alembic/templates/generic/alembic.ini.mako b/alembic/templates/generic/alembic.ini.mako index 3e211d0d..0a5a5754 100644 --- a/alembic/templates/generic/alembic.ini.mako +++ b/alembic/templates/generic/alembic.ini.mako @@ -12,9 +12,11 @@ script_location = ${script_location} # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s # sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. +# defaults to the current working directory. for multiple paths, the path separator +# is defined by "path_separator" below. prepend_sys_path = . + # timezone to use when rendering the date within the migration file # as well as the filename. # If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library. @@ -38,21 +40,35 @@ prepend_sys_path = . # version location specification; This defaults # to ${script_location}/versions. When using multiple version # directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" below. +# The path separator used here should be the separator specified by "path_separator" +# below. # version_locations = %(here)s/bar:%(here)s/bat:${script_location}/versions -# version path separator; As mentioned above, this is the character used to split -# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. -# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. -# Valid values for version_path_separator are: +# path_separator; This indicates what character is used to split lists of file +# paths, including version_locations and prepend_sys_path within configparser +# files such as alembic.ini. +# The default rendered in new alembic.ini files is "os", which uses os.pathsep +# to provide os-dependent path splitting. +# +# Note that in order to support legacy alembic.ini files, this default does NOT +# take place if path_separator is not present in alembic.ini. If this +# option is omitted entirely, fallback logic is as follows: +# +# 1. Parsing of the version_locations option falls back to using the legacy +# "version_path_separator" key, which if absent then falls back to the legacy +# behavior of splitting on spaces and/or commas. +# 2. Parsing of the prepend_sys_path option falls back to the legacy +# behavior of splitting on spaces, commas, or colons. +# +# Valid values for path_separator are: # -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -# version_path_separator = newline +# path_separator = : +# path_separator = ; +# path_separator = space +# path_separator = newline # # Use os.pathsep. Default configuration used for new projects. -version_path_separator = os +path_separator = os # set to 'true' to search source files recursively # in each "version_locations" directory diff --git a/alembic/templates/multidb/alembic.ini.mako b/alembic/templates/multidb/alembic.ini.mako index 00316456..a0cae1d5 100644 --- a/alembic/templates/multidb/alembic.ini.mako +++ b/alembic/templates/multidb/alembic.ini.mako @@ -12,7 +12,8 @@ script_location = ${script_location} # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s # sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. +# defaults to the current working directory. for multiple paths, the path separator +# is defined by "path_separator" below. prepend_sys_path = . # timezone to use when rendering the date within the migration file @@ -38,21 +39,35 @@ prepend_sys_path = . # version location specification; This defaults # to ${script_location}/versions. When using multiple version # directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" below. +# The path separator used here should be the separator specified by "path_separator" +# below. # version_locations = %(here)s/bar:%(here)s/bat:${script_location}/versions -# version path separator; As mentioned above, this is the character used to split -# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. -# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. -# Valid values for version_path_separator are: +# path_separator; This indicates what character is used to split lists of file +# paths, including version_locations and prepend_sys_path within configparser +# files such as alembic.ini. +# The default rendered in new alembic.ini files is "os", which uses os.pathsep +# to provide os-dependent path splitting. # -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -# version_path_separator = newline +# Note that in order to support legacy alembic.ini files, this default does NOT +# take place if path_separator is not present in alembic.ini. If this +# option is omitted entirely, fallback logic is as follows: +# +# 1. Parsing of the version_locations option falls back to using the legacy +# "version_path_separator" key, which if absent then falls back to the legacy +# behavior of splitting on spaces and/or commas. +# 2. Parsing of the prepend_sys_path option falls back to the legacy +# behavior of splitting on spaces, commas, or colons. +# +# Valid values for path_separator are: +# +# path_separator = : +# path_separator = ; +# path_separator = space +# path_separator = newline # # Use os.pathsep. Default configuration used for new projects. -version_path_separator = os +path_separator = os # set to 'true' to search source files recursively # in each "version_locations" directory diff --git a/alembic/testing/__init__.py b/alembic/testing/__init__.py index 0407adfe..809de7e4 100644 --- a/alembic/testing/__init__.py +++ b/alembic/testing/__init__.py @@ -15,6 +15,7 @@ from .assertions import assert_raises_message from .assertions import emits_python_deprecation_warning from .assertions import eq_ from .assertions import eq_ignore_whitespace +from .assertions import expect_deprecated from .assertions import expect_raises from .assertions import expect_raises_message from .assertions import expect_sqlalchemy_deprecated diff --git a/alembic/testing/assertions.py b/alembic/testing/assertions.py index c08b2228..898fbd16 100644 --- a/alembic/testing/assertions.py +++ b/alembic/testing/assertions.py @@ -167,6 +167,10 @@ def emits_python_deprecation_warning(*messages): return decorate +def expect_deprecated(*messages, **kw): + return _expect_warnings(DeprecationWarning, messages, **kw) + + def expect_sqlalchemy_deprecated(*messages, **kw): return _expect_warnings(sa_exc.SADeprecationWarning, messages, **kw) diff --git a/alembic/testing/env.py b/alembic/testing/env.py index 9a457b7f..a97990e6 100644 --- a/alembic/testing/env.py +++ b/alembic/testing/env.py @@ -157,6 +157,7 @@ script_location = {dir_} sqlalchemy.url = {url} sqlalchemy.future = {"true" if sqlalchemy_future else "false"} sourceless = {"true" if sourceless else "false"} +path_separator = space version_locations = %(here)s/model1/ %(here)s/model2/ %(here)s/model3/ \ {extra_version_location} diff --git a/alembic/util/__init__.py b/alembic/util/__init__.py index 786baa2b..1d3a2179 100644 --- a/alembic/util/__init__.py +++ b/alembic/util/__init__.py @@ -20,6 +20,7 @@ from .messaging import msg as msg from .messaging import obfuscate_url_pw as obfuscate_url_pw from .messaging import status as status from .messaging import warn as warn +from .messaging import warn_deprecated as warn_deprecated from .messaging import write_outstream as write_outstream from .pyfiles import coerce_resource_to_filename as coerce_resource_to_filename from .pyfiles import load_python_file as load_python_file diff --git a/alembic/util/messaging.py b/alembic/util/messaging.py index a2dbefa6..4c08f16e 100644 --- a/alembic/util/messaging.py +++ b/alembic/util/messaging.py @@ -81,6 +81,10 @@ def warn(msg: str, stacklevel: int = 2) -> None: warnings.warn(msg, UserWarning, stacklevel=stacklevel) +def warn_deprecated(msg: str, stacklevel: int = 2) -> None: + warnings.warn(msg, DeprecationWarning, stacklevel=stacklevel) + + def msg( msg: str, newline: bool = True, flush: bool = False, quiet: bool = False ) -> None: diff --git a/docs/build/branches.rst b/docs/build/branches.rst index 4b74ca0b..3e8bee1e 100644 --- a/docs/build/branches.rst +++ b/docs/build/branches.rst @@ -521,8 +521,8 @@ that module. So to start out, we can edit ``alembic.ini`` to refer to multiple directories; we'll also state the current ``versions`` directory as one of them:: - # A separator for the location paths must be defined first. - version_path_separator = os # Use os.pathsep. + # A separator for the location paths needs to be defined + path_separator = os # Use os.pathsep. # version location specification; this defaults # to foo/versions. When using multiple version # directories, initial revisions must be specified with --version-path diff --git a/docs/build/changelog.rst b/docs/build/changelog.rst index 0f0effe1..32a77a1f 100644 --- a/docs/build/changelog.rst +++ b/docs/build/changelog.rst @@ -4,7 +4,7 @@ Changelog ========== .. changelog:: - :version: 1.15.3 + :version: 1.16.0 :include_notes_from: unreleased .. changelog:: diff --git a/docs/build/tutorial.rst b/docs/build/tutorial.rst index 347e856b..74b5252a 100644 --- a/docs/build/tutorial.rst +++ b/docs/build/tutorial.rst @@ -166,16 +166,33 @@ The file generated with the "generic" configuration looks like:: # The path separator used here should be the separator specified by "version_path_separator" below. # version_locations = %(here)s/bar:%(here)s/bat:${script_location}/versions - # version path separator; As mentioned above, this is the character used to split - # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. - # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. - # Valid values for version_path_separator are: + # path_separator (New in Alembic 1.16.0, supersedes version_path_separator); + # This indicates what character is used to + # split lists of file paths, including version_locations and prepend_sys_path + # within configparser files such as alembic.ini. # - # version_path_separator = : - # version_path_separator = ; - # version_path_separator = space - # version_path_separator = newline - version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + # The default rendered in new alembic.ini files is "os", which uses os.pathsep + # to provide os-dependent path splitting. + # + # Note that in order to support legacy alembic.ini files, this default does NOT + # take place if path_separator is not present in alembic.ini. If this + # option is omitted entirely, fallback logic is as follows: + # + # 1. Parsing of the version_locations option falls back to using the legacy + # "version_path_separator" key, which if absent then falls back to the legacy + # behavior of splitting on spaces and/or commas. + # 2. Parsing of the prepend_sys_path option falls back to the legacy + # behavior of splitting on spaces, commas, or colons. + # + # Valid values for path_separator are: + # + # path_separator = : + # path_separator = ; + # path_separator = space + # path_separator = newline + # + # Use os.pathsep. Default configuration used for new projects. + path_separator = os # set to 'true' to search source files recursively # in each "version_locations" directory @@ -299,7 +316,7 @@ This file contains the following features: * ``timezone`` - an optional timezone name (e.g. ``UTC``, ``EST5EDT``, etc.) that will be applied to the timestamp which renders inside the migration file's comment as well as within the filename. This option requires Python>=3.9 - or installing the ``backports.zoneinfo`` library and the ``tzdata`` library. + or installing the ``backports.zoneinfo`` library and the ``tzdata`` library. If ``timezone`` is specified, the create date object is no longer derived from ``datetime.datetime.now()`` and is instead generated as:: @@ -341,9 +358,8 @@ This file contains the following features: allow revisions to exist in multiple directories simultaneously. See :ref:`multiple_bases` for examples. -* ``version_path_separator`` - a separator of ``version_locations`` paths. - It should be defined if multiple ``version_locations`` is used. - See :ref:`multiple_bases` for examples. +* ``path_separator`` - a separator character for the ``version_locations`` + and ``prepend_sys_path`` path lists. See :ref:`multiple_bases` for examples. * ``recursive_version_locations`` - when set to 'true', revision files are searched recursively in each "version_locations" directory. diff --git a/docs/build/unreleased/1330.rst b/docs/build/unreleased/1330.rst new file mode 100644 index 00000000..736149fd --- /dev/null +++ b/docs/build/unreleased/1330.rst @@ -0,0 +1,28 @@ +.. change:: + :tags: bug, environment + :tickets: 1330 + + Added new option to the ConfigParser config (typically via ``alembic.ini``) + ``path_separator``. This new option supersedes the previous similar + option ``version_path_separator``. The new ``path_separator`` option + applies to the path splitting mechanism of both the ``version_locations`` + option as well as the ``prepend_sys_path`` option, and in newly + rendered ``alembic.ini`` files will use the value ``os``, which means to + use the operating system path separator when splitting these string values + into a list of paths. + + The new attribute applies necessary os-dependent path splitting to the + ``prepend_sys_path`` option so that windows paths which contain drive + letters with colons are not inadvertently split, whereas previously + os-dependent path splitting were only available for the + ``version_locations`` option. + + Existing installations that do not have ``path_separator`` present + will continue to make use of ``version_path_separator`` when parsing the + ``version_locations`` option, or splitting on spaces / commas if + ``version_path_separator`` is also not present. ``prepend_sys_path`` will + continue to be split on spaces/commas/colons if ``path_separator`` is + not present. Under all of these fallback conditions, a deprecation + warning is now emitted encouraging to set ``path_separator``. + + Pull request courtesy Mike Werezak. \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py index 0fad0dda..cf85f516 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,4 +1,5 @@ import os +import sys import tempfile from alembic import config @@ -176,7 +177,7 @@ class ConfigTest(TestBase): "|", "/foo|/bar", ValueError( - "'|' is not a valid value for version_path_separator; " + "'|' is not a valid value for path_separator; " "expected 'space', 'newline', 'os', ':', ';'" ), ), @@ -187,7 +188,7 @@ class ConfigTest(TestBase): cfg = config.Config() if separator is not None: cfg.set_main_option( - "version_path_separator", + "path_separator", separator, ) cfg.set_main_option("script_location", tempfile.gettempdir()) @@ -198,9 +199,143 @@ class ConfigTest(TestBase): with expect_raises_message(ValueError, message, text_exact=True): ScriptDirectory.from_config(cfg) else: - s = ScriptDirectory.from_config(cfg) + if separator is None: + with testing.expect_deprecated( + "No path_separator found in configuration; " + "falling back to legacy splitting on spaces/commas " + "for version_locations" + ): + s = ScriptDirectory.from_config(cfg) + else: + s = ScriptDirectory.from_config(cfg) + eq_(s.version_locations, expected_result) + @testing.combinations( + ( + "legacy raw string 1", + None, + "/foo", + ["/foo"], + ), + ( + "legacy raw string 2", + None, + "/foo /bar", + ["/foo", "/bar"], + ), + ( + "legacy raw string 3", + "space", + "/foo", + ["/foo"], + ), + ( + "legacy raw string 4", + "space", + "/foo /bar", + ["/foo", "/bar"], + ), + ( + "multiline string 1", + "newline", + " /foo \n/bar ", + ["/foo", "/bar"], + ), + ( + "Linux pathsep 1", + ":", + "/Project A", + ["/Project A"], + ), + ( + "Linux pathsep 2", + ":", + "/Project A:/Project B", + ["/Project A", "/Project B"], + ), + ( + "Windows pathsep 1", + ";", + r"C:\Project A", + [r"C:\Project A"], + ), + ( + "Windows pathsep 2", + ";", + r"C:\Project A;C:\Project B", + [r"C:\Project A", r"C:\Project B"], + ), + ( + "os pathsep", + "os", + r"path_number_one%(sep)spath_number_two%(sep)s" + % {"sep": os.pathsep}, + [r"path_number_one", r"path_number_two"], + ), + ( + "invalid pathsep 2", + "|", + "/foo|/bar", + ValueError( + "'|' is not a valid value for path_separator; " + "expected 'space', 'newline', 'os', ':', ';'" + ), + ), + id_="iaaa", + argnames="separator, string_value, expected_result", + ) + def test_prepend_sys_path_locations( + self, separator, string_value, expected_result + ): + cfg = config.Config() + if separator is not None: + cfg.set_main_option( + "path_separator", + separator, + ) + cfg.set_main_option("script_location", tempfile.gettempdir()) + cfg.set_main_option("prepend_sys_path", string_value) + + if isinstance(expected_result, ValueError): + message = str(expected_result) + with expect_raises_message(ValueError, message, text_exact=True): + ScriptDirectory.from_config(cfg) + else: + restore_path = list(sys.path) + try: + sys.path.clear() + + if separator is None: + with testing.expect_deprecated( + "No path_separator found in configuration; " + "falling back to legacy splitting on spaces, commas, " + "and colons for prepend_sys_path" + ): + ScriptDirectory.from_config(cfg) + else: + ScriptDirectory.from_config(cfg) + eq_(sys.path, expected_result) + finally: + sys.path = restore_path + + def test_version_path_separator_deprecation_warning(self): + cfg = config.Config() + cfg.set_main_option("script_location", tempfile.gettempdir()) + cfg.set_main_option("version_path_separator", "space") + cfg.set_main_option( + "version_locations", "/path/one /path/two /path:/three" + ) + with testing.expect_deprecated( + "The version_path_separator configuration parameter is " + "deprecated; please use path_separator" + ): + script = ScriptDirectory.from_config(cfg) + eq_( + script.version_locations, + ["/path/one", "/path/two", "/path:/three"], + ) + class StdoutOutputEncodingTest(TestBase): def test_plain(self): diff --git a/tests/test_environment.py b/tests/test_environment.py index 5fc70401..fb98bf11 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -159,17 +159,28 @@ class CWDTest(TestBase): @testing.combinations( ( ".", + None, ["."], ), - ("/tmp/foo:/tmp/bar", ["/tmp/foo", "/tmp/bar"]), - ("/tmp/foo /tmp/bar", ["/tmp/foo", "/tmp/bar"]), - ("/tmp/foo,/tmp/bar", ["/tmp/foo", "/tmp/bar"]), - (". /tmp/foo", [".", "/tmp/foo"]), + ("/tmp/foo:/tmp/bar", None, ["/tmp/foo", "/tmp/bar"]), + ("/tmp/foo:/tmp/bar", ":", ["/tmp/foo", "/tmp/bar"]), + ("/tmp/foo /tmp/bar", None, ["/tmp/foo", "/tmp/bar"]), + ("/tmp/foo,/tmp/bar", None, ["/tmp/foo", "/tmp/bar"]), + (". /tmp/foo", None, [".", "/tmp/foo"]), + (". /tmp/foo", "space", [".", "/tmp/foo"]), ) - def test_sys_path_prepend(self, config_value, expected): + def test_sys_path_prepend(self, config_value, path_separator, expected): + if path_separator is not None: + self.cfg.set_main_option("path_separator", path_separator) self.cfg.set_main_option("prepend_sys_path", config_value) - script = ScriptDirectory.from_config(self.cfg) + if path_separator is None: + with testing.expect_deprecated( + "No path_separator found in configuration;" + ): + script = ScriptDirectory.from_config(self.cfg) + else: + script = ScriptDirectory.from_config(self.cfg) env = EnvironmentContext(self.cfg, script) target = os.path.abspath(_get_staging_directory())