diff --git a/graphviz/_tools.py b/graphviz/_tools.py index 018425c9f2..97c7dbf0a6 100644 --- a/graphviz/_tools.py +++ b/graphviz/_tools.py @@ -106,6 +106,7 @@ def promote_pathlike_directory(directory: typing.Union[os.PathLike, str, None], def deprecate_positional_args(*, supported_number: int, + ignore_arg: typing.Optional[str] = None, category: typing.Type[Warning] = PendingDeprecationWarning, stacklevel: int = 1): """Mark supported_number of positional arguments as the maximum. @@ -113,6 +114,7 @@ def deprecate_positional_args(*, Args: supported_number: Number of positional arguments for which no warning is raised. + ignore_arg: Name of positional argument to ignore. category: Type of Warning to raise or None to return a nulldecorator returning the undecorated function. @@ -144,27 +146,39 @@ def decorator(func): signature = inspect.signature(func) argnames = [name for name, param in signature.parameters.items() if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD] + check_number = supported_number + if ignore_arg is not None: + ignored = [name for name in argnames if name == ignore_arg] + assert ignored, 'ignore_arg must be a positional arg' + check_number += len(ignored) + qualification = f' (ignoring {ignore_arg}))' + else: + qualification = '' + + deprecated = argnames[supported_number:] + assert deprecated log.debug('deprecate positional args: %s.%s(%r)', - func.__module__, func.__qualname__, - argnames[supported_number:]) + func.__module__, func.__qualname__, deprecated) + + # mangle function name in message for this package + func_name = func.__name__.lstrip('_') + func_name, sep, rest = func_name.partition('_legacy') + assert func_name and (not sep or not rest) + + s_ = 's' if supported_number > 1 else '' @functools.wraps(func) def wrapper(*args, **kwargs): - if len(args) > supported_number: + if len(args) > check_number: call_args = zip(argnames, args) - supported = itertools.islice(call_args, supported_number) - supported = dict(supported) + supported = dict(itertools.islice(call_args, check_number)) deprecated = dict(call_args) assert deprecated - func_name = func.__name__.lstrip('_') - func_name, sep, rest = func_name.partition('_legacy') - assert not set or not rest wanted = ', '.join(f'{name}={value!r}' for name, value in deprecated.items()) - warnings.warn(f'The signature of {func.__name__} will be reduced' - f' to {supported_number} positional args' - f' {list(supported)}: pass {wanted}' - ' as keyword arg(s)', + warnings.warn(f'The signature of {func_name} will be reduced' + f' to {supported_number} positional arg{s_}{qualification}' + f' {list(supported)}: pass {wanted} as keyword arg{s_}', stacklevel=stacklevel, category=category) diff --git a/graphviz/saving.py b/graphviz/saving.py index 0a95e8f523..0fbd648fbe 100644 --- a/graphviz/saving.py +++ b/graphviz/saving.py @@ -50,7 +50,7 @@ def filepath(self) -> str: """The target path for saving the DOT source file.""" return os.path.join(self.directory, self.filename) - @_tools.deprecate_positional_args(supported_number=2) + @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self') def save(self, filename: typing.Union[os.PathLike, str, None] = None, directory: typing.Union[os.PathLike, str, None] = None, *, skip_existing: typing.Optional[bool] = False) -> str: diff --git a/graphviz/sources.py b/graphviz/sources.py index 12ec163102..8ac476cef2 100644 --- a/graphviz/sources.py +++ b/graphviz/sources.py @@ -39,7 +39,7 @@ class Source(rendering.Render, saving.Save, """ @classmethod - @_tools.deprecate_positional_args(supported_number=2) + @_tools.deprecate_positional_args(supported_number=1, ignore_arg='cls') def from_file(cls, filename: typing.Union[os.PathLike, str], directory: typing.Union[os.PathLike, str, None] = None, format: typing.Optional[str] = None, @@ -73,7 +73,7 @@ def from_file(cls, filename: typing.Union[os.PathLike, str], renderer=renderer, formatter=formatter, loaded_from_path=filepath) - @_tools.deprecate_positional_args(supported_number=2) + @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self') def __init__(self, source: str, filename: typing.Union[os.PathLike, str, None] = None, directory: typing.Union[os.PathLike, str, None] = None, @@ -122,7 +122,7 @@ def source(self) -> str: source += '\n' return source - @_tools.deprecate_positional_args(supported_number=2) + @_tools.deprecate_positional_args(supported_number=1, ignore_arg='self') def save(self, filename: typing.Union[os.PathLike, str, None] = None, directory: typing.Union[os.PathLike, str, None] = None, *, skip_existing: typing.Optional[bool] = None) -> str: diff --git a/tests/test_all_classes.py b/tests/test_all_classes.py index 122bff1c53..abc0d31e50 100644 --- a/tests/test_all_classes.py +++ b/tests/test_all_classes.py @@ -210,7 +210,7 @@ def test_save_mocked(mocker, dot, filename='nonfilename', directory='nondirector mock_makedirs = mocker.patch('os.makedirs', autospec=True) mock_open = mocker.patch('builtins.open', mocker.mock_open()) - with pytest.deprecated_call(match=r'\b2 positional args\b'): + with pytest.deprecated_call(match=r'\b1 positional arg\b'): assert dot.save(filename, directory) == dot.filepath assert dot.filename == filename diff --git a/tests/test_sources.py b/tests/test_sources.py index 5872d0e952..3c41058fc7 100644 --- a/tests/test_sources.py +++ b/tests/test_sources.py @@ -43,7 +43,7 @@ def test_filepath(platform, source): def test_from_file(tmp_path, filename='hello.gv', directory='source_hello', data='digraph { hello -> world }', encoding='utf-8', - deprecation_match=r'\b2 positional args\b'): + deprecation_match=r'\b1 positional arg\b'): lpath = tmp_path / directory lpath.mkdir() (lpath / filename).write_text(data, encoding=encoding)