pypa / setuptools_scm

the blessed package to manage your versions by scm tags

Home Page:https://setuptools-scm.readthedocs.io/en/latest/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

LookupError: setuptools-scm was unable to detect version if [tool.setuptools_scm] is in pyproject.toml

befeleme opened this issue · comments

project: https://github.com/ionelmc/python-lazy-object-proxy/
download the source code, extract the tar archive and enter the folder: https://github.com/ionelmc/python-lazy-object-proxy/archive/refs/tags/v1.10.0.tar.gz

In pyproject.toml there is empty table [tool.setuptools_scm] defined.
In setup.py you can find the use_scm_version keyword argument with fallback_version explicitly defined.

setup(                                                                                                                  
      name='lazy-object-proxy',                                                                                           
      use_scm_version={                                                                                                   
          'local_scheme': 'dirty-tag',                                                                                    
          'write_to': 'src/lazy_object_proxy/_version.py',                                                                
          'fallback_version': '1.10.0',                                                                                   
      }, ...

Running SETUPTOOLS_SCM_DEBUG=1 python setup.py --version ends up with a LookupError:

DEBUG setuptools_scm.overrides dist name: lazy-object-proxy
DEBUG setuptools_scm.config check absolute root=. relative_to=pyproject.toml
DEBUG setuptools_scm.config file pyproject.toml
DEBUG setuptools_scm.entrypoints version_from_ep setuptools_scm.parse_scm in /home/ksurma/Downloads/python-lazy-object-proxy-1.10.0
DEBUG setuptools_scm.discover looking for ep setuptools_scm.parse_scm in /home/ksurma/Downloads/python-lazy-object-proxy-1.10.0
DEBUG setuptools_scm.entrypoints version_from_ep setuptools_scm.parse_scm_fallback in .
DEBUG setuptools_scm.discover looking for ep setuptools_scm.parse_scm_fallback in .
DEBUG setuptools_scm.discover found ep EntryPoint(name='pyproject.toml', value='setuptools_scm.fallbacks:fallback_version', group='setuptools_scm.parse_scm_fallback') in .
DEBUG setuptools_scm.entrypoints EntryPoint(name='pyproject.toml', value='setuptools_scm.fallbacks:fallback_version', group='setuptools_scm.parse_scm_fallback') found None
DEBUG setuptools_scm.discover found ep EntryPoint(name='setup.py', value='setuptools_scm.fallbacks:fallback_version', group='setuptools_scm.parse_scm_fallback') in .
DEBUG setuptools_scm.entrypoints EntryPoint(name='setup.py', value='setuptools_scm.fallbacks:fallback_version', group='setuptools_scm.parse_scm_fallback') found None
DEBUG setuptools_scm.config check absolute root=. relative_to=pyproject.toml
DEBUG setuptools_scm.config file pyproject.toml
Traceback (most recent call last):
  File "/home/ksurma/Downloads/python-lazy-object-proxy-1.10.0/setup.py", line 76, in <module>
    setup(
  File "/usr/lib/python3.12/site-packages/setuptools/__init__.py", line 107, in setup
    return distutils.core.setup(**attrs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/setuptools/_distutils/core.py", line 147, in setup
    _setup_distribution = dist = klass(attrs)
                                 ^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/setuptools/dist.py", line 496, in __init__
    _Distribution.__init__(
  File "/usr/lib/python3.12/site-packages/setuptools/_distutils/dist.py", line 283, in __init__
    self.finalize_options()
  File "/usr/lib/python3.12/site-packages/setuptools/dist.py", line 935, in finalize_options
    ep(self)
  File "/home/ksurma/Downloads/python-lazy-object-proxy-1.10.0/.eggs/setuptools_scm-8.0.4-py3.12.egg/setuptools_scm/_integration/setuptools.py", line 121, in infer_version
    _assign_version(dist, config)
  File "/home/ksurma/Downloads/python-lazy-object-proxy-1.10.0/.eggs/setuptools_scm-8.0.4-py3.12.egg/setuptools_scm/_integration/setuptools.py", line 56, in _assign_version
    _version_missing(config)
  File "/home/ksurma/Downloads/python-lazy-object-proxy-1.10.0/.eggs/setuptools_scm-8.0.4-py3.12.egg/setuptools_scm/_get_version_impl.py", line 112, in _version_missing
    raise LookupError(
LookupError: setuptools-scm was unable to detect version for /home/ksurma/Downloads/python-lazy-object-proxy-1.10.0.

Make sure you're either building from a fully intact git repository or PyPI tarballs. Most other sources (such as GitHub's tarballs, a git checkout without the .git folder) don't contain the necessary metadata and will not work.

I comment out the [tool.setuptools_scm] table and run SETUPTOOLS_SCM_DEBUG=1 python setup.py --version again.
No error. Version is correctly set. Please note the found EntryPoint name set to pyproject.toml while in fact all this data is read from setup.py:

DEBUG setuptools_scm.overrides dist name: lazy-object-proxy
DEBUG setuptools_scm.config check absolute root=. relative_to=pyproject.toml
DEBUG setuptools_scm.config file pyproject.toml
DEBUG setuptools_scm.entrypoints version_from_ep setuptools_scm.parse_scm in /home/ksurma/Downloads/python-lazy-object-proxy-1.10.0
DEBUG setuptools_scm.discover looking for ep setuptools_scm.parse_scm in /home/ksurma/Downloads/python-lazy-object-proxy-1.10.0
DEBUG setuptools_scm.entrypoints version_from_ep setuptools_scm.parse_scm_fallback in .
DEBUG setuptools_scm.discover looking for ep setuptools_scm.parse_scm_fallback in .
DEBUG setuptools_scm.discover found ep EntryPoint(name='pyproject.toml', value='setuptools_scm.fallbacks:fallback_version', group='setuptools_scm.parse_scm_fallback') in .
DEBUG setuptools_scm.fallbacks FALLBACK 1.10.0
INFO setuptools_scm.version version 1.10.0 -> 1.10.0
DEBUG setuptools_scm.entrypoints EntryPoint(name='pyproject.toml', value='setuptools_scm.fallbacks:fallback_version', group='setuptools_scm.parse_scm_fallback') found <ScmVersion 1.10.0 dist=0 node=None dirty=False branch=None>
DEBUG setuptools_scm.version scm version <ScmVersion 1.10.0 dist=0 node=None dirty=False branch=None>
DEBUG setuptools_scm.version config Configuration(relative_to='pyproject.toml', root='.', version_scheme='guess-next-dev', local_scheme='dirty-tag', tag_regex=re.compile('^(?:[\\w-]+-)?(?P<version>[vV]?\\d+(?:\\.\\d+){0,2}[^\\+]*)(?:\\+.*)?$'), parentdir_prefix_version=None, fallback_version='1.10.0', fallback_root='.', write_to='src/lazy_object_proxy/_version.py', write_to_template=None, version_file=None, version_file_template=None, parse=None, git_describe_command=None, dist_name='lazy-object-proxy', version_cls=<class 'packaging.version.Version'>, search_parent_directories=False, parent=PosixPath('.'))
DEBUG setuptools_scm.dump_version dump 1.10.0 into src/lazy_object_proxy/_version.py
1.10.0

We came across this issue while debugging a weird behavior of two Fedora's build tools: copr, where the build consistently fails and locally in mock with exactly the same environment, where no such issue arises, the version is read correctly. Could it be that the mere order in which files are read creates such discrepancies?

For the record, when we see a build failure in our build system:

  DEBUG setuptools_scm.config file pyproject.toml
  DEBUG setuptools_scm.entrypoints version_from_ep setuptools_scm.parse_scm in /builddir/build/BUILD/python-lazy-object-proxy-1.10.0
  DEBUG setuptools_scm.discover looking for ep setuptools_scm.parse_scm in /builddir/build/BUILD/python-lazy-object-proxy-1.10.0
  DEBUG setuptools_scm.entrypoints version_from_ep setuptools_scm.parse_scm_fallback in .
  DEBUG setuptools_scm.discover looking for ep setuptools_scm.parse_scm_fallback in .
  DEBUG setuptools_scm.discover found ep EntryPoint(name='pyproject.toml', value='setuptools_scm.fallbacks:fallback_version', group='setuptools_scm.parse_scm_fallback') in .
  DEBUG setuptools_scm.entrypoints EntryPoint(name='pyproject.toml', value='setuptools_scm.fallbacks:fallback_version', group='setuptools_scm.parse_scm_fallback') found None
  DEBUG setuptools_scm.discover found ep EntryPoint(name='setup.py', value='setuptools_scm.fallbacks:fallback_version', group='setuptools_scm.parse_scm_fallback') in .
  DEBUG setuptools_scm.entrypoints EntryPoint(name='setup.py', value='setuptools_scm.fallbacks:fallback_version', group='setuptools_scm.parse_scm_fallback') found None
  DEBUG setuptools_scm.config check absolute root=. relative_to=pyproject.toml
  DEBUG setuptools_scm.config file pyproject.toml

While when it actually works in a different one:

  DEBUG setuptools_scm.config file pyproject.toml
  DEBUG setuptools_scm.entrypoints version_from_ep setuptools_scm.parse_scm in /builddir/build/BUILD/python-lazy-object-proxy-1.10.0
  DEBUG setuptools_scm.discover looking for ep setuptools_scm.parse_scm in /builddir/build/BUILD/python-lazy-object-proxy-1.10.0
  DEBUG setuptools_scm.entrypoints version_from_ep setuptools_scm.parse_scm_fallback in .
  DEBUG setuptools_scm.discover looking for ep setuptools_scm.parse_scm_fallback in .
  DEBUG setuptools_scm.discover found ep EntryPoint(name='pyproject.toml', value='setuptools_scm.fallbacks:fallback_version', group='setuptools_scm.parse_scm_fallback') in .
  DEBUG setuptools_scm.fallbacks FALLBACK 1.10.0
  INFO setuptools_scm.version version 1.10.0 -> 1.10.0
  DEBUG setuptools_scm.entrypoints EntryPoint(name='pyproject.toml', value='setuptools_scm.fallbacks:fallback_version', group='setuptools_scm.parse_scm_fallback') found <ScmVersion 1.10.0 dist=0 node=None dirty=False branch=None>

The sources should be identical, so I suspect it's the order of files on the filesystem or similar thing we cannot affect.

what versions of setuptools and setuptools_scm are involved

the empty table should be removed upstream
i also recommend setting the pretend version var explicitly

This is setuptools_scm 8.0.4 with setuptools 68.2.2 -- I can test with a newer one...

I was gonna say that setuptools-68.2.2 blows up and setuptools-69.0.3 works correctly, but no, it's just flaky.

I reproduced with Python 3.12.1 and:

Package           Version
----------------- -------
packaging         23.2
pip               23.2.1
setuptools        69.0.3
setuptools-scm    8.0.4
typing_extensions 4.9.0

the empty table should be removed upstream

I don't know why they have that. It being empty makes me feel uncomfortable as well. However, at least it should error explicitly or behave consistently if that is the problem.

i also recommend setting the pretend version var explicitly

We can definitively do that when we build the package, but nevertheless, if fallback_version is supposed to work, I guess we should figure out what's happening here, no?

Ah wait, it never should hit fallbacks for GitHub tarballs

Use pypi sdists

Ah wait, it never should hit fallbacks for GitHub tarballs

Why not? It says "fallback". I am confused.

Use pypi sdists

To clarify: we have a workaround available, we are not trying to sort out how to do this, we are trying to figure out what's going on here.

The git archival filename of the project is incorrect, thus it's not found

So it's incorrect repo setup

What does "git archival filename of the project" mean? This happens on unpackaged tarball. If I rename the directory to lazy-object-proxy-1.10.0 it still happens.

The error is in the git repo

That Tag will never produce a viable tarball

I am sorry. I have no idea what you mean.

There is a fallback version set. Sometimes, it is respected, sometimes it is not. I have no idea yet why. If I delete .git-archival.txt I still have the same error. If I clone the repo and delete .git, I have the same error.

What is the meaning of fallback_version, if not the fallback version?

For the record, the error does indeed tell me that what I do is wrong: "Make sure you're either building from a fully intact git repository or PyPI tarballs. Most other sources (such as GitHub's tarballs, a git checkout without the .git folder) don't contain the necessary metadata and will not work."

However, things I don't understand:

  • Why does this work sometimes (no sure when yet)?
  • Why does this work when we delete pyproject.toml?
  • Why does it not fallback to the fallback version?

This is the smallest reproducer I can come up with. Has no git involved whatsoever.

I only have 2 files:

$ cat pyproject.toml 
[tool.setuptools_scm]

$ cat setup.py 
from setuptools import setup

setup(
    name='xxx',
    use_scm_version={
        'local_scheme': 'dirty-tag',
        'write_to': '_version.py',
        'fallback_version': '1.10.0',
    },
    setup_requires=['setuptools_scm'],
)

I create a venv and install latest setuptools and setuptools_scm into it:

$ python3.12 -m venv venv
$ venv/bin/pip install setuptools_scm setuptools
...
Successfully installed packaging-23.2 setuptools-69.0.3 setuptools_scm-8.0.4 typing-extensions-4.9.0

It works:

$ venv/bin/python setup.py --version
...
1.10.0

Now let me uninstall and install setuptools again. This should make no difference:

$ venv/bin/pip uninstall setuptools
...
  Successfully uninstalled setuptools-69.0.3

$ venv/bin/pip install setuptools
...
Successfully installed setuptools-69.0.3

Yet suddenly, it no longer works:

$ rm _version.py   # optional step, does not change the outcome
$ venv/bin/python setup.py --version
...
Traceback (most recent call last):
  File ".../scmscm/setup.py", line 3, in <module>
    setup(
  File ".../scmscm/venv/lib64/python3.12/site-packages/setuptools/__init__.py", line 103, in setup
    return distutils.core.setup(**attrs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../scmscm/venv/lib64/python3.12/site-packages/setuptools/_distutils/core.py", line 147, in setup
    _setup_distribution = dist = klass(attrs)
                                 ^^^^^^^^^^^^
  File ".../scmscm/venv/lib64/python3.12/site-packages/setuptools/dist.py", line 303, in __init__
    _Distribution.__init__(self, dist_attrs)
  File ".../scmscm/venv/lib64/python3.12/site-packages/setuptools/_distutils/dist.py", line 283, in __init__
    self.finalize_options()
  File ".../scmscm/venv/lib64/python3.12/site-packages/setuptools/dist.py", line 654, in finalize_options
    ep(self)
  File ".../scmscm/venv/lib64/python3.12/site-packages/setuptools_scm/_integration/setuptools.py", line 121, in infer_version
    _assign_version(dist, config)
  File ".../scmscm/venv/lib64/python3.12/site-packages/setuptools_scm/_integration/setuptools.py", line 56, in _assign_version
    _version_missing(config)
  File ".../scmscm/venv/lib64/python3.12/site-packages/setuptools_scm/_get_version_impl.py", line 112, in _version_missing
    raise LookupError(
LookupError: setuptools-scm was unable to detect version for .../scmscm.

Make sure you're either building from a fully intact git repository or PyPI tarballs. Most other sources (such as GitHub's tarballs, a git checkout without the .git folder) don't contain the necessary metadata and will not work.

For example, if you're using pip, instead of https://github.com/user/proj/archive/master.zip use git+https://github.com/user/proj.git#egg=proj

A script:

cat > pyproject.toml << EOF
[tool.setuptools_scm]
EOF

cat > setup.py  << EOF
from setuptools import setup

setup(
    name='xxx',
    use_scm_version={
        'local_scheme': 'dirty-tag',
        'write_to': '_version.py',
        'fallback_version': '1.10.0',
    },
    setup_requires=['setuptools_scm'],
)
EOF

python3.12 -m venv venv
venv/bin/pip install -Uq setuptools_scm setuptools

venv/bin/python setup.py --version  # works

venv/bin/pip uninstall -qy setuptools
venv/bin/pip install -q setuptools

venv/bin/python setup.py --version  # breaks

That particular error is indeed unexpected

@hroncok thanks for that minimal repro case - I've been investigating a similar problem with the python-trx-python package in Debian, where a _version.py file is unpredictably created-or-not at package build-time -- it has a (non-empty) [tool.setuptools_scm] config table in pyproject.toml.

I believe the unpredictable behaviour is likely to be from the same cause, and that it relates to the order that the version_keyword and infer_version functions in setuptools_scm are invoked during package build.

Findings (so far)

Both the version_keyword and infer_version functions are declared as entrypoints in entry_points.txt file of setuptools_scm:

  • [project.entry-points."distutils.setup_keywords"]
    use_scm_version = "setuptools_scm._integration.setuptools:version_keyword"
  • [project.entry-points."setuptools.finalize_distribution_options"]
    setuptools_scm = "setuptools_scm._integration.setuptools:infer_version"

In the case of version_keyword, the function is placed in the distutils.setup_keywords group, handled by _finalize_setup_keywords in setuptools -- and therefore ultimately becomes part of the same finalize_distribution_options group where infer_version is found. This occurs indirectly, via the entry_points.txt file of setuptools, separate to the entrypoints file of setuptools_scm.

The finalize_distribution_options entrypoint group is a documented distribution-customization feature of setuptools.

Although the finalize_distribution_options group does allow functions to declare an order attribute on their callables to declare a runtime evaluation order, the default ordering is undefined (all items are defaulted to zero) and lacks a tiebreaker comparison.

Issue #1022 seems to relate to this area too.

Observations / possible solutions

What I find locally following installation of setuptools from recent tagged source versions (v69.2.0 for example) is that without changing any of the library code, I can get your minimal test case to pass/fail reliably by simply adjusting the version number in setuptools:setup.cfg and reinstalling it. That doesn't make much sense except to indicate a possible source of nondeterminism that does not relate to the code within setuptools itself, yet that can somehow be affected by (re)installations of the package. Ordering of reads from the filesystem, file timestamps, file paths and cache locations spring to mind as possible explanations, but I haven't confirmed the specific cause in this case yet.

I do think that the nondeterminism could be mitigated somewhere around the finalize_options function by creating a reliable tiebreaker for sort-ordering of the entrypoint functions, or allocating fixed order values for them, but I think it would be better to determine what the origin of the ordering difference is before patching over it.