From 0b9893b03e99cc9f5d5ec827242f4fb7285cf1e8 Mon Sep 17 00:00:00 2001 From: Zoran Simic Date: Wed, 11 Nov 2026 21:28:12 -0800 Subject: [PATCH 1/2] Code modernized for py3.10+ --- .github/workflows/release.yml | 2 +- .github/workflows/tests.yml | 4 +- CHANGELOG.md | 10 +++++ docs/contributing.md | 6 ++- examples/single/expected.txt | 2 +- examples/single/setup.py | 1 - pyproject.toml | 5 +++ setupmeta/__init__.py | 45 +++++++++++----------- setupmeta/commands.py | 43 ++++++++++----------- setupmeta/license.py | 6 +-- setupmeta/model.py | 49 ++++++++++++------------ setupmeta/scm.py | 28 +++++++------- setupmeta/versioning.py | 59 ++++++++++++++--------------- tests/__init__.py | 2 - tests/conftest.py | 4 +- tests/scenarios.py | 9 ++--- tests/scenarios/pinned/expected.txt | 4 +- tests/scenarios/pinned/setup.py | 1 - tests/test_commands.py | 2 +- tests/test_content.py | 2 +- tests/test_versioning.py | 2 +- tox.ini | 25 +++++------- 22 files changed, 156 insertions(+), 155 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 555164a..469d0a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: git config --global user.name "Tester" git config --global user.email "test@example.com" - - uses: astral-sh/setup-uv@v10.3.2 + - uses: astral-sh/setup-uv@v10.3.3 - run: uvx --with tox-uv tox -e py,docs,style - run: uv build - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8710ae9..49fa6de 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,7 +31,7 @@ jobs: git config --global user.name "Tester" git config --global user.email "test@example.com" - - uses: astral-sh/setup-uv@v10.3.2 + - uses: astral-sh/setup-uv@v10.3.3 - run: uvx --with tox-uv tox -e py - uses: coverallsapp/github-action@v2 @@ -59,5 +59,5 @@ jobs: with: python-version: "3.15" - - uses: astral-sh/setup-uv@v10.3.2 + - uses: astral-sh/setup-uv@v10.3.3 - run: uvx --with tox-uv tox -e docs,style diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c865e8..d5bbb07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 4.0.1 + +- Internal project modernizations: + + - Code modernized for py3.10+ (f-strings, removed py2 leftovers), enabled ruff `UP` rule + + - Using [check-sdist](https://pypi.org/project/check-sdist/) via `twine check --strict` + + - Bumped GitHub actions + ## 4.0.0 - 2026-10-27 ### Changed diff --git a/docs/contributing.md b/docs/contributing.md index 65bedb0..fc2531b 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -9,8 +9,10 @@ time, a bug breaks other people's builds: test coverage is kept at 100%. oldest and newest Python, and `coverage` combines both runs: that's the cheap way to get full coverage locally, CI covers the versions in between. -`tox -e docs` runs `check-manifest`, and the README render check PyPI relies on. Files added to -the repo must be covered by [MANIFEST.in](../MANIFEST.in) or ignored in `tox.ini`. +`tox -e docs` runs `check-sdist` (sdist contents must match git-tracked files), then builds the +sdist and wheel and runs `twine check --strict` on them (the README render check PyPI relies on). +Files added to the repo must be covered by [MANIFEST.in](../MANIFEST.in) or listed under +`[tool.check-sdist]` in [pyproject.toml](../pyproject.toml). ## Scenario snapshots diff --git a/examples/single/expected.txt b/examples/single/expected.txt index e0acddf..1d62839 100644 --- a/examples/single/expected.txt +++ b/examples/single/expected.txt @@ -3,7 +3,7 @@ \_: (single.py:5 ) Someone someone@example.com author_email: (auto-adjust ) someone@example.com description: (README.rst:1) A package implemented by a single .py file - \_: (setup.py:3 ) Unicode chars in setup.py 😀 😎 + \_: (setup.py:2 ) Unicode chars in setup.py 😀 😎 download_url: (missing ) - Consider specifying 'download_url' license: (single.py:4 ) MIT long_description: (README.rst ) single: A package implemented by a single .py file ================================================== This example shows: * `name` ... diff --git a/examples/single/setup.py b/examples/single/setup.py index 2fabf76..9b5484d 100644 --- a/examples/single/setup.py +++ b/examples/single/setup.py @@ -1,4 +1,3 @@ -# coding=utf-8 """ Unicode chars in setup.py 😀 😎 """ diff --git a/pyproject.toml b/pyproject.toml index 56512fc..f0114ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,9 +2,13 @@ requires = ["setuptools"] build-backend = "setuptools.build_meta" +[tool.check-sdist] +git-only = [".gitignore", "AGENTS.md"] + [tool.ruff] cache-dir = ".tox/.ruff_cache" line-length = 140 +target-version = "py310" [tool.ruff.lint] ignore = ["RUF005", "RUF012"] @@ -38,6 +42,7 @@ select = [ "TCH", # flake8-type-checking # "TD", # flake8-todos "TRY", # tryceratops + "UP", # pyupgrade "W", # pycodestyle warnings ] [tool.ruff.lint.per-file-ignores] diff --git a/setupmeta/__init__.py b/setupmeta/__init__.py index cd0375b..477f4fe 100644 --- a/setupmeta/__init__.py +++ b/setupmeta/__init__.py @@ -27,9 +27,9 @@ PKGID = "[A-Za-z0-9][-A-Za-z0-9_.]*" # Simplistic parsing of known formats used in requirements.txt -RE_SIMPLE_PIN = re.compile(r"^(%s)\s*==\s*([^;\s]+)\s*(;.*)?$" % PKGID) +RE_SIMPLE_PIN = re.compile(rf"^({PKGID})\s*==\s*([^;\s]+)\s*(;.*)?$") RE_WORDS = re.compile(r"\W+") -RE_PKG_NAME = re.compile(r"^(%s)$" % PKGID) +RE_PKG_NAME = re.compile(rf"^({PKGID})$") ABSTRACT = "abstract" INDIRECT = "indirect" @@ -268,11 +268,11 @@ def quoted(text): def _strs(value, bracket, first, sep, quote, indent): """Stringified iterable""" if isinstance(value, dict): - rep = sep.join("%s: %s" % (stringify(k, quote=quote), stringify(v, quote=quote, indent=indent)) for k, v in sorted(value.items())) - return "{%s%s%s}" % (first, rep, first[:-4]) + rep = sep.join(f"{stringify(k, quote=quote)}: {stringify(v, quote=quote, indent=indent)}" for k, v in sorted(value.items())) + return f"{{{first}{rep}{first[:-4]}}}" quote = quote or quote is None - return "%s%s%s%s%s" % (bracket[0], first, sep.join(stringify(s, quote=quote, indent=indent) for s in value), first[:-4], bracket[1]) + return f"{bracket[0]}{first}{sep.join(stringify(s, quote=quote, indent=indent) for s in value)}{first[:-4]}{bracket[1]}" def _strm(value, bracket, quote, indent, chars=80): @@ -281,8 +281,8 @@ def _strm(value, bracket, quote, indent, chars=80): if len(value) <= 1 or len(result) <= chars: return result - sep = ",\n%s" % indent if indent else ", " - first = "\n%s" % indent if indent else "" + sep = f",\n{indent}" if indent else ", " + first = f"\n{indent}" if indent else "" return _strs(value, bracket, first, sep, quote, indent) @@ -298,12 +298,12 @@ def stringify(value, quote=None, indent=""): return _strm(value, "{}", quote=quote, indent=indent) if callable(value): - return "function '%s'" % value.__name__ + return f"function '{value.__name__}'" if quote: - return quoted("%s" % value) + return quoted(f"{value}") - return "%s" % value + return f"{value}" def listify(text, separator=None): @@ -335,7 +335,7 @@ def readlines(relative_path, limit=0): try: result = [] full_path = project_path(relative_path) - with open(full_path, "rt") as fh: + with open(full_path) as fh: for line in fh: limit -= 1 if limit == 0: @@ -343,9 +343,9 @@ def readlines(relative_path, limit=0): result.append(line) - trace("read %s lines from %s" % (len(result), relative_path)) + trace(f"read {len(result)} lines from {relative_path}") - except IOError: + except OSError: return None else: @@ -406,7 +406,7 @@ def standard_req(line): return line -class ReqEntry(object): +class ReqEntry: def __init__(self, parent, source_path, line_number, parent_section, line): """ :param RequirementsFile parent: Requirements.txt file where this line came from @@ -473,8 +473,8 @@ def __init__(self, parent, source_path, line_number, parent_section, line): prev = self.requirement name = m.group(1) spec = m.group(3) - self.requirement = name if not spec else "%s%s" % (name, spec) - trace(" abstracted [%s] -> [%s]" % (prev, self.requirement)) + self.requirement = name if not spec else f"{name}{spec}" + trace(f" abstracted [{prev}] -> [{self.requirement}]") self.abstracted = True def __repr__(self): @@ -501,13 +501,13 @@ def section(self): @property def source_description(self): - msg = "from %s:%s" % (self.source or "adhoc", self.line_number) + msg = f"from {self.source or 'adhoc'}:{self.line_number}" if self.abstracted is not None: if self.local_section: - msg += ", '%s' stated on line" % self.local_section + msg += f", '{self.local_section}' stated on line" elif self.parent_section: - msg += ", in '%s' section" % self.parent_section + msg += f", in '{self.parent_section}' section" elif self.abstracted: msg += ", abstracted by default" @@ -548,11 +548,10 @@ def iterate_req_txt(seen, parent, source_path, lines): continue - trace(" req entry: %s" % req_entry) + trace(f" req entry: {req_entry}") if req_entry.refers and req_entry.refers not in seen: seen.add(req_entry.refers) - for r in iterate_req_txt(seen, parent, req_entry.refers, readlines(req_entry.refers)): - yield r + yield from iterate_req_txt(seen, parent, req_entry.refers, readlines(req_entry.refers)) elif req_entry.requirement not in seen: seen.add(req_entry.requirement) @@ -632,7 +631,7 @@ def find_requirements(*relative_paths): path = project_path(path) if os.path.isfile(path): do_abstract = not path.endswith(".in") - trace("found requirements: %s %s" % (path, " (auto-abstracted)" if do_abstract else "")) + trace(f"found requirements: {path} {' (auto-abstracted)' if do_abstract else ''}") r = RequirementsFile.from_file(path, do_abstract=do_abstract) if r is not None: return r diff --git a/setupmeta/commands.py b/setupmeta/commands.py index dc52de6..8c30164 100644 --- a/setupmeta/commands.py +++ b/setupmeta/commands.py @@ -61,12 +61,8 @@ def _show_requirements_synopsis(self): """Show how many requirements were auto-abstracted or ignored, if any""" reqs = self.setupmeta.requirements and self.setupmeta.requirements.install_requires if reqs and reqs.filled_requirements and (reqs.abstracted or reqs.ignored): - message = "[setupmeta] install_requires: %s abstracted, %s ignored, %s untouched" % ( - len(reqs.abstracted), - len(reqs.ignored), - len(reqs.untouched), - ) - print(message) + counts = f"{len(reqs.abstracted)} abstracted, {len(reqs.ignored)} ignored, {len(reqs.untouched)} untouched" + print(f"[setupmeta] install_requires: {counts}") def _show_git_status(self): if self.setupmeta.versioning: @@ -74,7 +70,7 @@ def _show_git_status(self): if scm: diff = scm.get_diff_report() if diff: - print("Pending changes:\n%s" % diff) + print(f"Pending changes:\n{diff}") @MetaCommand @@ -141,13 +137,13 @@ def finalize_options(self): def check_recommend(self, key, hint=None): if key not in self.setupmeta.definitions: - hint = ", %s" % hint if hint else "" - self.setupmeta.auto_fill(key, "- Consider specifying '%s'%s" % (key, hint), "missing") + hint = f", {hint}" if hint else "" + self.setupmeta.auto_fill(key, f"- Consider specifying '{key}'{hint}", "missing") def represented_req(self, name, source_description, align): - name = '"%s",' % name + name = f'"{name}",' if source_description: - fmt = "%%-%ss# %%s" % align + fmt = f"%-{align}s# %s" name = fmt % (name, source_description) return name @@ -172,9 +168,10 @@ def show_requirements(self, setup_key, requirements): for i, name in enumerate(names): content.append(self.represented_req(name, source_descriptions[i], longest_name)) - content = "[\n %s\n ]," % "\n ".join(content).strip() + indent = "\n " + content = f"[{indent}{indent.join(content).strip()}\n ]," - print(" %s=%s" % (setup_key, content)) + print(f" {setup_key}={content}") def show_dependencies(self): """Copy-pastable code snippet with install_requires""" @@ -193,7 +190,7 @@ def show_expanded_python(self): version = definitions.get("version") if version: - print("__version__ = %s\n\n" % setupmeta.stringify(version.value, quote=True)) + print(f"__version__ = {setupmeta.stringify(version.value, quote=True)}\n\n") print("setup(") @@ -215,13 +212,13 @@ def show_expanded_python(self): elif definition.key == "download_url": if version and version.value in definition.value: definition.value = definition.value.replace(version.value, "%s") - definition.value = "%s %% __version__" % setupmeta.stringify(setupmeta.short(definition.value), quote=True) + definition.value = f"{setupmeta.stringify(setupmeta.short(definition.value), quote=True)} % __version__" else: definition.value = setupmeta.stringify(definition.value, quote=True, indent=" ") elif definition.key == "long_description": - definition.value = "open(%s).read()" % setupmeta.stringify(setupmeta.short(definition.source), quote=True) + definition.value = f"open({setupmeta.stringify(setupmeta.short(definition.source), quote=True)}).read()" elif definition.key == "version": definition.value = "__version__" @@ -235,18 +232,18 @@ def show_expanded_python(self): longest = longest_line([d.value for d in defs]) for definition in defs: if definition.key == "versioning": - line = " # versioning=%s," % definition.value + line = f" # versioning={definition.value}," else: - line = " %s=%s," % (definition.key, definition.value) + line = f" {definition.key}={definition.value}," source = definition.actual_source if source and source != "explicit": - comment = "# from %s" % setupmeta.short(source) + comment = f"# from {setupmeta.short(source)}" rest, _, last_line = line.rpartition("\n") padding = " " * max(1, longest - len(last_line)) - last_line = "%s%s%s" % (last_line, padding, comment) - line = "%s\n%s" % (rest, last_line) if rest else last_line + last_line = f"{last_line}{padding}{comment}" + line = f"{rest}\n{last_line}" if rest else last_line print(line) @@ -279,7 +276,7 @@ def run(self): longest_key = min(30, max(len(key) for key in definitions)) sources = [s for d in definitions.values() for s in d.sources] longest_source = min(40, max(len(s.source) for s in sources)) - form = "%%%ss: (%%%ss) %%s" % (longest_key, -longest_source) + form = f"%{longest_key}s: (%{-longest_source}s) %s" max_chars = max(60, self.chars - longest_key - longest_source - 5) for definition in sorted(definitions.values()): @@ -288,7 +285,7 @@ def run(self): prefix = "\\_" elif source.key not in setupmeta.MetaDefs.all_fields: - prefix = "%s*" % source.key + prefix = f"{source.key}*" else: prefix = source.key diff --git a/setupmeta/license.py b/setupmeta/license.py index 60e7cda..f4c04f2 100644 --- a/setupmeta/license.py +++ b/setupmeta/license.py @@ -42,13 +42,13 @@ def match(self, contents): pre = "Affero " if version: - post = "v%s" % version[0] + post = f"v{version[0]}" - short = "%sGPL%s" % (pre and pre[0], post) + short = f"{pre and pre[0]}GPL{post}" if short == "Apache" and version: # Most project seem to abbreviate this to "Apache 2.0" - short = "%s %s" % (self.short, version) + short = f"{self.short} {version}" return short diff --git a/setupmeta/model.py b/setupmeta/model.py index 6324381..b442254 100644 --- a/setupmeta/model.py +++ b/setupmeta/model.py @@ -3,7 +3,6 @@ """ import inspect -import io import os import re import sys @@ -44,7 +43,7 @@ RE_DOC_VALUE = re.compile(r"^([a-z_]+)\s*[:=]\s*(.+?)(\s*#.+)?$") # Match PKG-INFO metadata, of the form: Some-Key: some value -RE_PKG_KEY_VALUE = re.compile(r"^(%s):\s?(.*)$" % PKGID) +RE_PKG_KEY_VALUE = re.compile(rf"^({PKGID}):\s?(.*)$") # Beautify short description RE_DESCRIPTION = re.compile(r"^[\W\s]*((([\w\-]+)\s*[:-])?\s*(.+))$", re.IGNORECASE) @@ -89,10 +88,10 @@ def __init__(self, key, value, source): self.source = source def __repr__(self): - return "%s=%s from %s" % (self.key, short(self.value), self.source) + return f"{self.key}={short(self.value)} from {self.source}" -class Definition(object): +class Definition: """Record definitions for a given key, and where they were found""" def __init__(self, key): @@ -104,8 +103,8 @@ def __init__(self, key): self.sources = [] # type: list[DefinitionEntry] def __repr__(self): - source = self.sources[0].source if len(self.sources) == 1 else "%s sources" % len(self.sources) - return "%s=%s from %s" % (self.key, short(self.value), source) + source = self.sources[0].source if len(self.sources) == 1 else f"{len(self.sources)} sources" + return f"{self.key}={short(self.value)} from {source}" def __eq__(self, other): return isinstance(other, Definition) and self.key is other.key @@ -131,7 +130,7 @@ def merge_sources(self, sources): for entry in sources: if not self.value and entry.value: self.value = entry.value - trace("[-- %s] %s=%s" % (entry.source, self.key, entry.value)) + trace(f"[-- {entry.source}] {self.key}={entry.value}") self.sources.append(entry) @@ -151,11 +150,11 @@ def add(self, value, source, override=False): entry = DefinitionEntry(self.key, value, source) if override: self.sources.insert(0, entry) - trace("[<- %s] %s=%s" % (source, self.key, short(value))) + trace(f"[<- {source}] {self.key}={short(value)}") else: self.sources.append(entry) - trace("[-> %s] %s=%s" % (source, self.key, short(value))) + trace(f"[-> {source}] {self.key}={short(value)}") @property def is_meaningful(self): @@ -171,7 +170,7 @@ def __init__(self): def __repr__(self): project_dir = short(MetaDefs.project_dir) - return "%s definitions, %s" % (len(self.definitions), project_dir) + return f"{len(self.definitions)} definitions, {project_dir}" def value(self, key): """Value currently associated to 'key', if any""" @@ -224,7 +223,7 @@ def __init__(self, *relative_paths): self.full_path = project_path(*relative_paths) self.exists = os.path.isfile(self.full_path) if self.exists: - with io.open(self.full_path, "rt") as fh: + with open(self.full_path) as fh: docstring_marker = None docstring_start = None docstring = [] @@ -260,7 +259,7 @@ def add_pair(self, key, value, line, **kwargs): if key and value: source = self.relative_path if line: - source = "%s:%s" % (source, line) + source = f"{source}:{line}" self.add_definition(key, value, source, **kwargs) @@ -376,7 +375,7 @@ def __init__(self, root): self.info[key].append(line[8:].rstrip()) elif line.strip(): - trace("Unknown format line %s in %s: %s" % (line_number, self.path, line)) + trace(f"Unknown format line {line_number} in {self.path}: {line}") self.name = self.info.get("name") self.pythonified_name = pythonified_name(self.name) @@ -403,7 +402,7 @@ def load_more_info(self, folder, depth=3): if not self.name or not os.path.isdir(folder) or depth <= 0: return False - path = os.path.join(folder, "%s.egg-info" % self.pythonified_name) + path = os.path.join(folder, f"{self.pythonified_name}.egg-info") if os.path.isdir(path): self.entry_points_txt = self.checked_file(path, "entry_points.txt") self.requires_txt = self.checked_file(path, "requires.txt") @@ -495,9 +494,9 @@ def finalize(self, upstream): name = self.pythonified_name src_folder = project_path("src") if os.path.isdir(src_folder): - trace("looking for src packages in %s" % src_folder) + trace(f"looking for src packages in {src_folder}") packages = setuptools.find_packages(where=src_folder) - if not packages and os.path.isfile(project_path("src", "%s.py" % name)): + if not packages and os.path.isfile(project_path("src", f"{name}.py")): py_modules = [name] if packages or py_modules: @@ -506,7 +505,7 @@ def finalize(self, upstream): else: src_folder = project_path() if os.path.isdir(src_folder): - trace("looking for direct packages in %s" % src_folder) + trace(f"looking for direct packages in {src_folder}") with current_folder(src_folder): raw_packages = setuptools.find_packages() if raw_packages: @@ -514,9 +513,9 @@ def finalize(self, upstream): # For any other use-case, user must explicitly list their packages packages = [p for p in raw_packages if p.startswith(name)] if packages != raw_packages: - trace("all packages found: %s" % raw_packages) + trace(f"all packages found: {raw_packages}") - if not packages and os.path.isfile(project_path("%s.py" % name)): + if not packages and os.path.isfile(project_path(f"{name}.py")): py_modules = [name] if packages: @@ -527,7 +526,7 @@ def finalize(self, upstream): # Scan the usual/conventional places for py_module in py_modules: - self.merge(SimpleModule("%s.py" % py_module)) + self.merge(SimpleModule(f"{py_module}.py")) for package in packages: if package and "." not in package: @@ -616,17 +615,17 @@ def find_project_dir(setup_py_path): module = inspect.getmodule(frame[0]) if module and is_setup_py_path(module.__file__): setup_py_path = module.__file__ - trace("setup.py found from call stack: %s" % setup_py_path) + trace(f"setup.py found from call stack: {setup_py_path}") break if not setup_py_path and sys.argv and is_setup_py_path(sys.argv[0]): setup_py_path = sys.argv[0] - trace("setup.py found from sys.argv: %s" % setup_py_path) + trace(f"setup.py found from sys.argv: {setup_py_path}") if is_setup_py_path(setup_py_path): setup_py_path = os.path.abspath(setup_py_path) MetaDefs.project_dir = os.path.dirname(setup_py_path) - trace("project dir: %s" % MetaDefs.project_dir) + trace(f"project dir: {MetaDefs.project_dir}") def extract_short_description(self, contents): """ @@ -667,7 +666,7 @@ def auto_fill_long_description(self): best_long = value if short_desc: - self.auto_fill("description", short_desc, source="%s:1" % readme) + self.auto_fill("description", short_desc, source=f"{readme}:1") break self.add_definition("long_description", best_long, best_readme) @@ -677,7 +676,7 @@ def auto_fill_entry_points(self, key="entry_points"): if self.pkg_info.entry_points_txt: self.add_definition(key, load_contents(self.pkg_info.entry_points_txt), relative_path(self.pkg_info.entry_points_txt)) - path = "%s.ini" % key + path = f"{key}.ini" self.add_definition(key, load_contents(path), path) def auto_fill_license(self): diff --git a/setupmeta/scm.py b/setupmeta/scm.py index cfc2178..12481a9 100644 --- a/setupmeta/scm.py +++ b/setupmeta/scm.py @@ -20,7 +20,7 @@ def __init__(self, root): self.root = root def __repr__(self): - return "%s %s" % (self.name, self.root) + return f"{self.name} {self.root}" def is_dirty(self): """ @@ -143,7 +143,7 @@ def parsed_git_describe(text, origin=None): return Version(main=main, distance=distance, commitid=commitid, dirty=dirty, text=text) if origin: - setupmeta.warn("Ignoring invalid version from %s: %s" % (origin, text)) + setupmeta.warn(f"Ignoring invalid version from {origin}: {text}") return Version(main="0.0.0", dirty=True) def is_dirty(self): @@ -172,13 +172,13 @@ def git_describe_output(self): override = os.environ.get("SETUPMETA_GIT_DESCRIBE_COMMAND") if override: # Override was given, just use it as-is - setupmeta.trace("Using SETUPMETA_GIT_DESCRIBE_COMMAND: %s" % override) + setupmeta.trace(f"Using SETUPMETA_GIT_DESCRIBE_COMMAND: {override}") cmd = override.split(" ") return self.git_output(*cmd) version_tag = self.version_tag if version_tag: - setupmeta.trace("Using configured version_tag: %s" % version_tag) + setupmeta.trace(f"Using configured version_tag: {version_tag}") else: version_tag = "v*.*" @@ -193,7 +193,7 @@ def get_version(self): # Try harder commitid = self.git_output("rev-parse", "--short", "HEAD") - commitid = "g%s" % commitid if commitid else "" + commitid = f"g{commitid}" if commitid else "" distance = self.git_output("rev-list", "HEAD") distance = distance.count("\n") + 1 if distance else 0 return Version(main=None, distance=distance, commitid=commitid, dirty=self.is_dirty()) @@ -210,7 +210,7 @@ def commit_files(self, commit, push, relative_paths, next_version): relative_paths = sorted(set(relative_paths)) self.run_git("add", *relative_paths, dryrun=not commit, passthrough=True) - self.run_git("commit", "-m", "Version %s" % next_version, "--no-verify", dryrun=not commit, passthrough=True) + self.run_git("commit", "-m", f"Version {next_version}", "--no-verify", dryrun=not commit, passthrough=True) if push: if self.has_origin(): self.run_git("push", "origin", dryrun=not commit, passthrough=True) @@ -227,10 +227,10 @@ def apply_tag(self, commit, push, next_version, branch): state = m.group(6) if state and ("behind" in state or "gone" in state): # Example: Local branch 'main' is out of date (behind 1), can't bump - setupmeta.abort("Local branch '%s' is out of date (%s), can't bump" % (branch, state)) + setupmeta.abort(f"Local branch '{branch}' is out of date ({state}), can't bump") - bump_msg = "Version %s" % next_version - tag = "v%s" % next_version + bump_msg = f"Version {next_version}" + tag = f"v{next_version}" self.run_git("tag", "-a", tag, "-m", bump_msg, dryrun=not commit, passthrough=True) if push: @@ -336,7 +336,7 @@ def __init__(self, main=None, distance=0, commitid=None, dirty=False, text=None) self.commitid = (commitid or "g0000000").strip() self.dirty = ".dirty" if dirty else "" main = (main or "0.0.0").strip() - self.text = text or "v%s-%s-%s" % (main, self.distance, self.commitid) + self.text = text or f"v{main}-{self.distance}-{self.commitid}" self.major, self.minor, self.patch, self.additional, _, _ = setupmeta.version_components(main) def __repr__(self): @@ -345,7 +345,7 @@ def __repr__(self): @property def main_text(self): """Main components only""" - return "%s.%s.%s" % (self.major, self.minor, self.patch) + return f"{self.major}.{self.minor}.{self.patch}" @property def post(self): @@ -355,7 +355,7 @@ def post(self): :return str: '.post{distance}' for distance > 0, empty string otherwise """ if self.distance: - return "%s.post%s" % (self.additional, self.distance) + return f"{self.additional}.post{self.distance}" return self.additional @@ -367,7 +367,7 @@ def dev(self): :return str: '.dev{distance}' for distance > 0, empty string otherwise """ if self.distance or self.dirty: - return "%s.dev%s" % (self.additional, self.distance) + return f"{self.additional}.dev{self.distance}" return self.additional @@ -379,6 +379,6 @@ def devcommit(self): :return str: '.{commitid}' for distance > 0, empty string otherwise """ if self.distance or self.dirty: - return ".%s" % self.commitid + return f".{self.commitid}" return "" diff --git a/setupmeta/versioning.py b/setupmeta/versioning.py index 127b029..649dcc3 100644 --- a/setupmeta/versioning.py +++ b/setupmeta/versioning.py @@ -1,4 +1,3 @@ -import io import os import re @@ -54,7 +53,7 @@ def project_scm(root): if os.path.isfile(version_file): return Snapshot(root) - setupmeta.trace("could not determine SCM for '%s'" % root) + setupmeta.trace(f"could not determine SCM for '{root}'") return None @@ -73,7 +72,7 @@ def __init__(self, strategy, text, alternative=None, constant=False): self.renderer = self.rendered_env_var elif not hasattr(Version, self.text): - self.problem = "invalid versioning part '%s'" % self.text + self.problem = f"invalid versioning part '{self.text}'" else: self.renderer = self.rendered_attr @@ -81,11 +80,11 @@ def __init__(self, strategy, text, alternative=None, constant=False): def __repr__(self): text = self.text if self.alternative: - text = "%s:%s" % (text, self.alternative) + text = f"{text}:{self.alternative}" text = ("'%s'" if self.constant else "{%s}") % text if self.problem: - text = " [%s]" % self.problem + text = f" [{self.problem}]" return text @@ -155,7 +154,7 @@ def rendered_env_var(self, version): # noqa: ARG002 return None - return "%s%s" % (prefix, value) + return f"{prefix}{value}" def rendered(self, version): """ @@ -176,7 +175,7 @@ def __init__(self, main, extra, branches, **kwargs): self.extra = extra self.version_tag = kwargs.pop("version_tag", None) if kwargs: - setupmeta.warn("Ignored fields for 'versioning': %s" % kwargs) + setupmeta.warn(f"Ignored fields for 'versioning': {kwargs}") self.main_bits = self.bits(main) if isinstance(self.main_bits, list): @@ -219,7 +218,7 @@ def formatted(branches, main, extra): result += setupmeta.stringify(extra) if branches: - result = "branch(%s):%s" % (branches, result) + result = f"branch({branches}):{result}" return result @@ -280,7 +279,7 @@ def rendered(self, version, extra=True, auto_bumped=True): extra = "".join(extra) if extra: - result = "%s+%s" % (result, extra.strip(".")) + result = f"{result}+{extra.strip('.')}" return result @@ -303,12 +302,12 @@ def bumped(self, what, current_version): :return str: Represented next version, with 'what' bumped """ if not isinstance(self.main_bits, list): - setupmeta.abort("Can't bump with custom version definition: %s" % setupmeta.stringify(self.main_bits)) + setupmeta.abort(f"Can't bump with custom version definition: {setupmeta.stringify(self.main_bits)}") if what not in self.bumpable: - msg = "Can't bump '%s', it's out of scope" % what - msg += " of main format '%s'" % self.main - msg += " acceptable values: %s" % ", ".join(self.bumpable) + msg = f"Can't bump '{what}', it's out of scope" + msg += f" of main format '{self.main}'" + msg += f" acceptable values: {', '.join(self.bumpable)}" setupmeta.abort(msg) major, minor, rev = current_version.major, current_version.minor, current_version.patch @@ -321,7 +320,7 @@ def bumped(self, what, current_version): elif what == "patch": major, minor, rev = (major, minor, rev + 1) - next_version = Version(main="%s.%s.%s" % (major, minor, rev)) + next_version = Version(main=f"{major}.{minor}.{rev}") return self.rendered(next_version, extra=False) @classmethod @@ -368,7 +367,7 @@ def _parsed_versioning(given): for bit in reversed(to_be_moved): if bit not in extra: - extra = "%s%s" % (bit, extra) + extra = f"{bit}{extra}" main = main.strip(".") extra = extra.strip(".") @@ -426,7 +425,7 @@ def __init__(self, meta, scm): elif not self.scm: self.problem = "project not under a supported SCM" - setupmeta.trace("versioning given: '%s', strategy: [%s], problem: [%s]" % (given, self.strategy, self.problem)) + setupmeta.trace(f"versioning given: '{given}', strategy: [{self.strategy}], problem: [{self.problem}]") def auto_fill_version(self): """Autofill version as defined by 'self.strategy'""" @@ -447,18 +446,18 @@ def auto_fill_version(self): if self.strategy: setupmeta.warn(self.problem) - setupmeta.trace("not auto-filling version due to problem: [%s]" % self.problem) + setupmeta.trace(f"not auto-filling version due to problem: [{self.problem}]") return gv = self.scm.get_version() if self.generate_version_file: path = setupmeta.project_path(setupmeta.VERSION_FILE) with open(path, "w") as fh: - fh.write("%s" % gv) + fh.write(f"{gv}") if gv.patch and "patch" not in self.strategy.bumpable: - msg = "patch version component should be .0 for versioning strategy '%s', " % self.strategy - msg += "'.%s' from current version tag '%s' will be ignored" % (gv.patch, gv) + msg = f"patch version component should be .0 for versioning strategy '{self.strategy}', " + msg += f"'.{gv.patch}' from current version tag '{gv}' will be ignored" setupmeta.warn(msg) rendered = self.strategy.rendered(gv) @@ -468,7 +467,7 @@ def auto_fill_version(self): expected = gv.main_text if actual != expected: source = vdef.sources[0].source - msg = "In %s version should be '%s', not '%s'" % (source, expected, cv) + msg = f"In {source} version should be '{expected}', not '{cv}'" setupmeta.warn(msg) self.meta.auto_fill("version", rendered, self.scm.name, override=True) @@ -489,10 +488,10 @@ def verify_remote_tags(self): if remote_only: message = "Can't bump: not all remote tags are present locally!\n" if local_only: - message += "Tags only seen locally: %s\n" % ", ".join(local_only) + message += f"Tags only seen locally: {', '.join(local_only)}\n" if remote_only: - message += "Tags only on remote: %s\n" % ", ".join(remote_only) + message += f"Tags only on remote: {', '.join(remote_only)}\n" setupmeta.abort(message) @@ -502,7 +501,7 @@ def bump(self, what, commit=False, push=False, simulate_branch=None): branch = simulate_branch or self.scm.get_branch() if branch not in self.strategy.branches: - setupmeta.abort("Can't bump branch '%s', need one of %s" % (branch, self.strategy.branches)) + setupmeta.abort(f"Can't bump branch '{branch}', need one of {self.strategy.branches}") gv = self.scm.get_version() if gv and gv.dirty: @@ -541,7 +540,7 @@ def update_sources(self, next_version, commit, push, vdefs): changed = 0 line_number = 0 revised = None - with io.open(full_path, "rt") as fh: + with open(full_path) as fh: for line in fh.readlines(): line_number += 1 if line_number == target_line: @@ -553,16 +552,16 @@ def update_sources(self, next_version, commit, push, vdefs): lines.append(line) if not changed: - print("%s already has the right version" % vdef.source) + print(f"{vdef.source} already has the right version") else: modified.append(relative_path) if commit: - with io.open(full_path, "wt") as fh: + with open(full_path, "w") as fh: fh.writelines(lines) else: - print("Would update %s with: %s" % (vdef.source, revised.strip())) + print(f"Would update {vdef.source} with: {revised.strip()}") if not modified: return @@ -589,6 +588,6 @@ def updated_line(line, next_version): comment = "" if "#" in value: i = value.index("#") - comment = " #%s" % value[i + 1 :] + comment = f" #{value[i + 1 :]}" - return "%s%s%s%s%s%s%s\n" % (key, sep, space, quote, next_version, quote, comment) + return f"{key}{sep}{space}{quote}{next_version}{quote}{comment}\n" diff --git a/tests/__init__.py b/tests/__init__.py index 0b671df..eb788b3 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - """Unit test package for setupmeta""" diff --git a/tests/conftest.py b/tests/conftest.py index a6542bf..9f8149b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,7 +34,7 @@ def ignore_warning(*_, **__): def print_warning(message, *_, **__): """Print simplified warnings for capture in testing, instead of letting warnings do its funky thing""" - print("WARNING: %s" % setupmeta.short(message, -60)) + print(f"WARNING: {setupmeta.short(message, -60)}") def run_git(*args, cwd=None): @@ -247,7 +247,7 @@ def __init__(self, describe="v0.1.2-3-g123-dirty", branch="main", commitid="abc1 self.describe = describe self.branch = branch self.commitid = commitid - self.status_message = "## %s...origin/%s" % (branch, branch) + self.status_message = f"## {branch}...origin/{branch}" self._local_tags = local_tags self._remote_tags = remote_tags Git.__init__(self, TESTS) diff --git a/tests/scenarios.py b/tests/scenarios.py index 13e7396..64184aa 100644 --- a/tests/scenarios.py +++ b/tests/scenarios.py @@ -1,7 +1,6 @@ import argparse import contextlib import difflib -import io import logging import os import shutil @@ -92,7 +91,7 @@ def __init__(self, relative_path, in_place=False): extra_commands = os.path.join(self.folder, ".commands") if os.path.isfile(extra_commands): self.target = None - with io.open(extra_commands, "rt") as fh: + with open(extra_commands) as fh: for line in fh: line = line.strip() if line: @@ -149,7 +148,7 @@ def replay(self): result = [] for command in self.commands: output = conftest.spawn_setup_py(self.target, *command.split()) - output = ":: %s\n%s" % (command, output) + output = f":: {command}\n{output}" result.append(output) return "\n\n".join(result).rstrip() @@ -171,8 +170,8 @@ def refresh_example(self, dryrun): print(output) return - with io.open(self.expected_path(), "wt") as fh: - fh.write("%s\n" % output) + with open(self.expected_path(), "w") as fh: + fh.write(f"{output}\n") def main(): diff --git a/tests/scenarios/pinned/expected.txt b/tests/scenarios/pinned/expected.txt index 8429446..118c211 100644 --- a/tests/scenarios/pinned/expected.txt +++ b/tests/scenarios/pinned/expected.txt @@ -2,7 +2,7 @@ author: (auto-adjust) Someone \_: (pinned.py:5) Someone someone@example.com author_email: (auto-adjust) someone@example.com - description: (setup.py:3 ) This is a package that has setupmeta pinned to an explicit version range. + description: (setup.py:2 ) This is a package that has setupmeta pinned to an explicit version range. download_url: (missing ) - Consider specifying 'download_url' license: (pinned.py:4) MIT long_description: (missing ) - Consider specifying 'long_description', add a README file @@ -25,7 +25,7 @@ __version__ = "0.1.0" setup( author="Someone", # from pinned.py:5 author_email="someone@example.com", - description="This is a package that has setupmeta pinned to an explicit version range.", # from setup.py:3 + description="This is a package that has setupmeta pinned to an explicit version range.", # from setup.py:2 license="MIT", # from pinned.py:4 name="pinned", py_modules=["pinned"], diff --git a/tests/scenarios/pinned/setup.py b/tests/scenarios/pinned/setup.py index f6f3310..df216b6 100644 --- a/tests/scenarios/pinned/setup.py +++ b/tests/scenarios/pinned/setup.py @@ -1,4 +1,3 @@ -# coding=utf-8 """ This is a package that has setupmeta pinned to an explicit version range. """ diff --git a/tests/test_commands.py b/tests/test_commands.py index c8c0870..5bdf74b 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -14,7 +14,7 @@ def run_setup_py(args, expected, folder=None): line = line.strip() if line: m = re.search(line, output) - assert m, "'%s' not present in output of '%s': %s" % (line, " ".join(args), output) + assert m, f"'{line}' not present in output of {args}: {output}" def test_check(sample_project): diff --git a/tests/test_content.py b/tests/test_content.py index 4782c09..fd8ad22 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -20,7 +20,7 @@ def test_shortening(): path = os.path.expanduser("~/foo/bar") assert setupmeta.short(path) == "~/foo/bar" - assert setupmeta.short("found in %s" % path) == "found in ~/foo/bar" + assert setupmeta.short(f"found in {path}") == "found in ~/foo/bar" assert setupmeta.short({"foo": "bar"}, c=8) == "1 keys" diff --git a/tests/test_versioning.py b/tests/test_versioning.py index f3ff851..3f95654 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -286,7 +286,7 @@ def extra_version(version): return "extra" if version.distance: - return "d%s" % version.distance + return f"d{version.distance}" return "" diff --git a/tox.ini b/tox.ini index 3aee46d..ee7378a 100644 --- a/tox.ini +++ b/tox.ini @@ -7,8 +7,7 @@ indexserver = default = https://pypi.org/simple [testenv] -setenv = UV_CACHE_DIR={toxworkdir}/.uv-cache - COVERAGE_FILE={toxworkdir}/.coverage.{envname} +setenv = COVERAGE_FILE={toxworkdir}/.coverage.{envname} usedevelop = True deps = -rtests/requirements.txt py310: pip @@ -16,8 +15,7 @@ deps = -rtests/requirements.txt commands = pytest {posargs:-vv --cov=setupmeta --cov-report=xml tests} [testenv:coverage] -setenv = {[testenv]setenv} - COVERAGE_FILE={toxworkdir}/.coverage +setenv = COVERAGE_FILE={toxworkdir}/.coverage skip_install = True deps = coverage commands = coverage combine @@ -26,13 +24,13 @@ commands = coverage combine coverage html [testenv:docs] -setenv = {[testenv]setenv} - PYTHONWARNINGS=ignore skip_install = True -deps = check-manifest - readme-renderer -commands = check-manifest - python setup.py check --strict --restructuredtext +deps = build[uv] + check-sdist[uv] + twine +commands = check-sdist + python -m build --installer uv --outdir {envtmpdir}/dist + twine check --strict {envtmpdir}/dist/* [testenv:style] skip_install = True @@ -55,11 +53,6 @@ commands = {posargs:python --version} usedevelop = True commands = python tests/scenarios.py regen -[check-manifest] -ignore-bad-ideas = PKG-INFO -ignore = .setupmeta.version - AGENTS.md - [coverage:run] patch = subprocess data_file = .tox/.coverage @@ -68,3 +61,5 @@ data_file = .tox/.coverage output = .tox/test-reports/coverage.xml [coverage:html] directory = .tox/test-reports/htmlcov +[pytest] +cache_dir = .tox/pytest_cache From d1f0f3162d3d06cd4fd834d4f753a531e8aa7c01 Mon Sep 17 00:00:00 2001 From: Zoran Simic Date: Wed, 11 Nov 2026 22:23:47 -0800 Subject: [PATCH 2/2] Leaner sdist --- .github/workflows/tests.yml | 1 - CHANGELOG.md | 2 ++ MANIFEST.in | 6 +----- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 49fa6de..d9c67fc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,7 +37,6 @@ jobs: - uses: coverallsapp/github-action@v2 with: file: .tox/test-reports/coverage.xml - flag-name: python-${{ matrix.python-version }} parallel: true coveralls-finish: diff --git a/CHANGELOG.md b/CHANGELOG.md index d5bbb07..4df4979 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 4.0.1 +- Leaner sdist: no longer bundles `tests/`, `examples/`, `docs/` and `tox.ini` + - Internal project modernizations: - Code modernized for py3.10+ (f-strings, removed py2 leftovers), enabled ruff `UP` rule diff --git a/MANIFEST.in b/MANIFEST.in index 1ef2e27..d30b5d8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,4 @@ include LICENSE include CHANGELOG.md include README.rst -include tox.ini -recursive-include docs *.md -graft examples -graft tests -global-exclude __pycache__ *.py[cod] +prune tests diff --git a/pyproject.toml b/pyproject.toml index f0114ba..1a636fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.check-sdist] -git-only = [".gitignore", "AGENTS.md"] +git-only = [".gitignore", "AGENTS.md", "docs/", "examples/", "tests/", "tox.ini"] [tool.ruff] cache-dir = ".tox/.ruff_cache"