Skip to content
4 changes: 2 additions & 2 deletions autorelease/gh_actions_stages/autorelease-gh-rel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ jobs:
fi
name: "Install autorelease"
- run: |
VERSION=`python setup.py --version`
PROJECT=`python setup.py --name`
VERSION=`autorelease metadata version`
PROJECT=`autorelease metadata name`
echo $$PROJECT $$VERSION
autorelease-release --project $$PROJECT --version $$VERSION --token $$AUTORELEASE_TOKEN
env:
Expand Down
2 changes: 1 addition & 1 deletion autorelease/gh_actions_stages/autorelease-prep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
name: "Install release tools"
- run: |
bump-dev-version
python setup.py --version
autorelease metadata version
name: "Bump testpypi dev version"
- run: |
python setup.py sdist bdist_wheel
Expand Down
31 changes: 20 additions & 11 deletions autorelease/scripts/bump_dev_version.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import argparse
import time

try:
from configparser import ConfigParser, NoSectionError, NoOptionError
except ImportError:
# py2
from ConfigParser import ConfigParser, NoSectionError, NoOptionError


from json import JSONDecodeError

from packaging.version import Version
import requests
from autorelease.utils import split_setup_cfg_path
from autorelease.version import (
ConfigParserError, get_setup_cfg, get_setup_name, get_setup_version
)

def get_latest_pypi(package, index="https://test.pypi.org/pypi"):
url = "/".join([index, package, 'json'])
Expand Down Expand Up @@ -60,10 +57,22 @@ def shared_parser():
return parser

def get_version_info(conf_name, index):
conf = ConfigParser()
conf.read(conf_name)
v_setup = conf.get('metadata', 'version')
package = conf.get('metadata', 'name')
directory, filename = split_setup_cfg_path(conf_name)
try:
conf = get_setup_cfg(directory=directory, filename=filename)
except ConfigParserError as exc:
raise RuntimeError(
f"Unable to parse setup config: {conf_name}"
) from exc
if conf is None:
raise RuntimeError(f"Unable to find setup config: {conf_name}")

v_setup = get_setup_version(conf, default_version=None)
package = get_setup_name(conf, default_name=None)
if v_setup is None:
raise RuntimeError(f"Missing [metadata] version in {conf_name}")
if package is None:
raise RuntimeError(f"Missing [metadata] name in {conf_name}")
v_pypi = get_latest_pypi(package, index)
return conf, package, v_setup, v_pypi

Expand Down
50 changes: 50 additions & 0 deletions autorelease/scripts/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

from autorelease.scripts.vendor import vendor_actions
from autorelease.scripts.check import run_checks
from autorelease.utils import split_setup_cfg_path
from autorelease.version import (
ConfigParserError, get_setup_cfg, get_setup_name, get_setup_version
)
# from autorelease import ReleaseNoteWriter
from autorelease.gh_api4.notes4 import NotesWriter, prs_since_latest_release

Expand Down Expand Up @@ -77,6 +81,52 @@ def auth(auth):
auth = load_auth(auth)
pprint(auth)

@cli.group()
def metadata():
pass


@metadata.command(name="version")
@click.option("-c", "--conf", type=str, default="setup.cfg",
help="setup.cfg file to use")
def metadata_version(conf):
directory, filename = split_setup_cfg_path(conf)
try:
setup_cfg = get_setup_cfg(directory=directory, filename=filename)
except ConfigParserError as exc:
raise click.ClickException(
f"Unable to parse setup config: {conf}"
) from exc
value = get_setup_version(setup_cfg, default_version=None)
field = "version"

if value is None:
raise click.ClickException(
f"Missing [metadata] {field} in {conf}"
)
Comment thread
dwhswenson marked this conversation as resolved.
Comment thread
dwhswenson marked this conversation as resolved.
Comment thread
dwhswenson marked this conversation as resolved.
click.echo(value)


@metadata.command(name="name")
@click.option("-c", "--conf", type=str, default="setup.cfg",
help="setup.cfg file to use")
def metadata_name(conf):
directory, filename = split_setup_cfg_path(conf)
try:
setup_cfg = get_setup_cfg(directory=directory, filename=filename)
except ConfigParserError as exc:
raise click.ClickException(
f"Unable to parse setup config: {conf}"
) from exc
value = get_setup_name(setup_cfg, default_name=None)
field = "name"

if value is None:
raise click.ClickException(
f"Missing [metadata] {field} in {conf}"
)
Comment thread
dwhswenson marked this conversation as resolved.
Comment thread
dwhswenson marked this conversation as resolved.
Comment thread
dwhswenson marked this conversation as resolved.
click.echo(value)
Comment on lines +89 to +136

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two metadata commands (metadata_version and metadata_name) have significant code duplication. They differ only in the getter function called and the field name used. Consider refactoring to reduce duplication by creating a shared helper function or parameterizing the common logic. This would make the code more maintainable and reduce the chance of inconsistencies between the two commands.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deciding not to address this. The duplicated code is minimal.

Comment thread
dwhswenson marked this conversation as resolved.


@cli.command()
@click.option('--conf', type=click.File('r'))
Expand Down
49 changes: 49 additions & 0 deletions autorelease/tests/test_bump_dev_version.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pytest
from autorelease.scripts.bump_dev_version import *
import autorelease.scripts.bump_dev_version as bump_mod
import autorelease.version as version_mod

def test_shared_parser():
parser = shared_parser()
Expand All @@ -22,3 +24,50 @@ def test_select_version(v_pypi, v_setup, expected):
])
def test_bump_dev_version(version_str, expected):
assert bump_dev_version(version_str) == expected


def test_get_version_info_reuses_loaded_conf(tmp_path, monkeypatch):
setup_cfg = tmp_path / "setup.cfg"
setup_cfg.write_text(
"[metadata]\n"
"name = mypkg\n"
"version = 1.2.3.dev0\n"
)

real_get_setup_cfg = bump_mod.get_setup_cfg
calls = {"count": 0}

def counted_get_setup_cfg(*args, **kwargs):
calls["count"] += 1
return real_get_setup_cfg(*args, **kwargs)

def unexpected_get_setup_cfg(*args, **kwargs):
raise AssertionError("setup.cfg was reloaded")

monkeypatch.setattr(bump_mod, "get_setup_cfg", counted_get_setup_cfg)
monkeypatch.setattr(version_mod, "get_setup_cfg", unexpected_get_setup_cfg)
monkeypatch.setattr(bump_mod, "get_latest_pypi", lambda *args: "1.2.2")

conf, package, v_setup, v_pypi = bump_mod.get_version_info(
str(setup_cfg), "https://example.invalid/pypi"
)

assert calls["count"] == 1
assert conf.get("metadata", "name") == "mypkg"
assert package == "mypkg"
assert v_setup == "1.2.3.dev0"
assert v_pypi == "1.2.2"


def test_get_version_info_malformed_cfg(tmp_path):
setup_cfg = tmp_path / "setup.cfg"
setup_cfg.write_text(
"[metadata\n"
"name = badpkg\n"
"version = 0.0.0\n"
)

with pytest.raises(RuntimeError, match="Unable to parse setup config"):
bump_mod.get_version_info(
str(setup_cfg), "https://example.invalid/pypi"
)
66 changes: 65 additions & 1 deletion autorelease/tests/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import os


from autorelease.version import _find_rel_path_for_file
from autorelease.version import (
ConfigParserError, _find_rel_path_for_file, get_setup_cfg,
get_setup_name, get_setup_version
)

@pytest.mark.parametrize("depth, result", [
(0, '.'), (1, '..'), (2, '..' + os.sep + '..'),
Expand All @@ -25,3 +28,64 @@ def test_find_rel_path_for_file_finds_no_file():
with mock.patch('autorelease.version.os.path.isfile', lambda x: False):
assert _find_rel_path_for_file(-1, 'setup.cfg') is None


def test_get_setup_name_and_version(tmp_path):
setup_cfg = tmp_path / "setup.cfg"
setup_cfg.write_text(
"[metadata]\n"
"name = mypkg\n"
"version = 1.2.3.dev0\n"
)
conf = get_setup_cfg(str(tmp_path), "setup.cfg")
assert get_setup_name(conf, default_name=None) == "mypkg"
assert get_setup_version(conf, default_version=None) == "1.2.3.dev0"


def test_get_setup_name_and_version_missing_file(tmp_path):
default_name = "default-name"
default_version = "0.0.0"
conf = get_setup_cfg(str(tmp_path), "setup.cfg")
assert get_setup_name(conf, default_name=default_name) == default_name
assert get_setup_version(
conf, default_version=default_version
) == default_version


def test_get_setup_name_and_version_missing_fields(tmp_path):
setup_cfg_no_name = tmp_path / "setup_no_name.cfg"
setup_cfg_no_name.write_text(
"[metadata]\n"
"version = 2.0.0\n"
)
conf_no_name = get_setup_cfg(str(tmp_path), "setup_no_name.cfg")
assert get_setup_name(
conf_no_name, default_name="default-name"
) == "default-name"
assert get_setup_version(
conf_no_name, default_version=None
) == "2.0.0"

setup_cfg_no_version = tmp_path / "setup_no_version.cfg"
setup_cfg_no_version.write_text(
"[metadata]\n"
"name = pkg-without-version\n"
)
conf_no_version = get_setup_cfg(str(tmp_path), "setup_no_version.cfg")
assert get_setup_version(
conf_no_version, default_version="0.0.0"
) == "0.0.0"
assert get_setup_name(
conf_no_version, default_name=None
) == "pkg-without-version"


def test_get_setup_name_and_version_malformed_cfg(tmp_path):
setup_cfg = tmp_path / "setup.cfg"
setup_cfg.write_text(
"[metadata\n"
"name = badpkg\n"
"version = 0.0.0\n"
)

with pytest.raises(ConfigParserError):
get_setup_cfg(str(tmp_path), "setup.cfg")
11 changes: 10 additions & 1 deletion autorelease/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import yaml
import re
import os
import sys

def conda_recipe_version(recipe_file):
import yaml
with open(recipe_file) as f:
dct = yaml.load(f.read(), Loader=yaml.FullLoader)
return dct['package']['version']
Expand Down Expand Up @@ -35,3 +35,12 @@ def _import_setup_py3(directory):
setup = importlib.util.module_from_spec(spec)
spec.loader.exec_module(setup)
return setup


def split_setup_cfg_path(conf_name):
directory, filename = os.path.split(conf_name)
if directory == "":
directory = "."
if filename == "":
filename = "setup.cfg"
return directory, filename
53 changes: 43 additions & 10 deletions autorelease/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
import subprocess

try:
from configparser import ConfigParser, NoSectionError, NoOptionError
from configparser import (
ConfigParser, Error as ConfigParserError, NoSectionError, NoOptionError
)
except ImportError:
# py2
from ConfigParser import ConfigParser, NoSectionError, NoOptionError
from ConfigParser import (
ConfigParser, Error as ConfigParserError, NoSectionError, NoOptionError
)

try:
from ._installed_version import _installed_version
Expand Down Expand Up @@ -98,6 +102,11 @@ def get_setup_cfg(directory, filename="setup.cfg"):
directory for setup.cfg, relative to cwd; default '.'
filename : str
filename for setup.cfg; default 'setup.cfg'

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get_setup_cfg docstring still says it loads setup.cfg "as a dict-of-dict", but the function actually returns a ConfigParser instance (or None). Updating the docstring (including the parameter docs here) to reflect the real return type would avoid confusion for callers.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 995399c


Raises
------
ConfigParserError
if setup.cfg exists but cannot be parsed
"""
if isinstance(directory, int):
rel_path = _find_rel_path_for_file(directory, filename)
Expand All @@ -114,20 +123,44 @@ def get_setup_cfg(directory, filename="setup.cfg"):
return conf


def get_setup_version(default_version, directory, filename="setup.cfg"):
version = default_version
conf = get_setup_cfg(directory, filename)
def get_setup_value(conf, option, default=None, section='metadata'):
"""Get a setup.cfg value or return the provided default.

Parameters
----------
conf : ConfigParser
loaded setup.cfg content to query
option : str
field name to retrieve from section
default
value to return when setup.cfg is missing or does not define field
section : str
section name to query; default 'metadata'
"""
value = default
try:
version = conf.get('metadata', 'version')
value = conf.get(section, option)
except (NoSectionError, NoOptionError):
pass # version (or metadata) not defined in setup.cfg
pass # option (or section) not defined in setup.cfg
except AttributeError:
pass # no setup.cfg found (conf is None)
return version
return value


def get_setup_version(conf, default_version=None):
return get_setup_value(conf, option='version', section='metadata',
default=default_version)


def get_setup_name(conf, default_name=None):
return get_setup_value(conf, option='name', section='metadata',
default=default_name)


short_version = get_setup_version(_installed_version,
directory=_version_setup_depth)
short_version = get_setup_version(
get_setup_cfg(directory=_version_setup_depth),
default_version=_installed_version,
)
_git_version = get_git_version()
_is_repo = (_git_version != '' and _git_version != "Unknown")

Expand Down
6 changes: 4 additions & 2 deletions autorelease/version_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import re
import packaging.version as vers

from autorelease.version import get_setup_version # reuse the vendored
from autorelease.version import get_setup_cfg, get_setup_version
from autorelease.utils import conda_recipe_version

import importlib
Expand All @@ -17,7 +17,9 @@ def import_and_get(fully_qualified):


version_getters = {
'setup-cfg': lambda path: get_setup_version(None, path),
'setup-cfg': lambda path: get_setup_version(
get_setup_cfg(path), default_version=None
),
'conda': conda_recipe_version,
'getattr': import_and_get,
}
Expand Down
Loading
Loading