Repository: pypa/setuptools

Issue:
[BUG] `ConfigDiscovery.analyse_name` incorrectly sets the `name` attribute on `Distribution`
### setuptools version

setuptools==65.1.0

### Python version

Python 3.10.2

### OS

macOS 12.5, Oracle Linux Server release 8.6

### Additional environment information

_No response_

### Description

Under certain circumstances (see reproduction steps) `setuptools.discovery.ConfigDiscovery.analyse_name` will overwrite the `name` attribute of the `Distribution` object leading to `python setup.py --name` failing.

### Expected behavior

```
$ python setup.py --name
my_package_name
```

### How to Reproduce

A project demonstrating this issue (along with a detailed description of what's going on) can be found here:

https://github.com/jeamland/setuptools-name-bug

### Output

```console
$ python setup.py --name
/Users/benno/.pyenv/versions/setuptools-name-bug/lib/python3.10/site-packages/setuptools/config/pyprojecttoml.py:108: _BetaConfiguration: Support for `[tool.setuptools]` in `pyproject.toml` is still *beta*.
  warnings.warn(msg, _BetaConfiguration)
Traceback (most recent call last):
  File "/Users/benno/work/setuptools-name-bug/setup.py", line 3, in <module>
    setup()
  File "/Users/benno/.pyenv/versions/setuptools-name-bug/lib/python3.10/site-packages/setuptools/__init__.py", line 87, in setup
    return distutils.core.setup(**attrs)
  File "/Users/benno/.pyenv/versions/setuptools-name-bug/lib/python3.10/site-packages/setuptools/_distutils/core.py", line 170, in setup
    ok = dist.parse_command_line()
  File "/Users/benno/.pyenv/versions/setuptools-name-bug/lib/python3.10/site-packages/setuptools/_distutils/dist.py", line 471, in parse_command_line
    args = parser.getopt(args=self.script_args, object=self)
  File "/Users/benno/.pyenv/versions/setuptools-name-bug/lib/python3.10/site-packages/setuptools/_distutils/fancy_getopt.py", line 274, in getopt
    val = getattr(object, attr, 0) + 1
TypeError: can only concatenate str (not "int") to str
```



Discussion:

Resolving patch:
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -234,8 +234,8 @@ def expand(self):
 
         # A distribution object is required for discovering the correct package_dir
         dist = self._ensure_dist()
-
-        with _EnsurePackagesDiscovered(dist, self.setuptools_cfg) as ensure_discovered:
+        ctx = _EnsurePackagesDiscovered(dist, self.project_cfg, self.setuptools_cfg)
+        with ctx as ensure_discovered:
             package_dir = ensure_discovered.package_dir
             self._expand_data_files()
             self._expand_cmdclass(package_dir)
@@ -428,8 +428,11 @@ def _ignore_errors(ignore_option_errors: bool):
 
 
 class _EnsurePackagesDiscovered(_expand.EnsurePackagesDiscovered):
-    def __init__(self, distribution: "Distribution", setuptools_cfg: dict):
+    def __init__(
+        self, distribution: "Distribution", project_cfg: dict, setuptools_cfg: dict
+    ):
         super().__init__(distribution)
+        self._project_cfg = project_cfg
         self._setuptools_cfg = setuptools_cfg
 
     def __enter__(self):
@@ -443,8 +446,10 @@ def __enter__(self):
 
         dist.set_defaults._ignore_ext_modules()  # pyproject.toml-specific behaviour
 
-        # Set `py_modules` and `packages` in dist to short-circuit auto-discovery,
-        # but avoid overwriting empty lists purposefully set by users.
+        # Set `name`, `py_modules` and `packages` in dist to short-circuit
+        # auto-discovery, but avoid overwriting empty lists purposefully set by users.
+        if dist.metadata.name is None:
+            dist.metadata.name = self._project_cfg.get("name")
         if dist.py_modules is None:
             dist.py_modules = cfg.get("py-modules")
         if dist.packages is None:
diff --git a/setuptools/discovery.py b/setuptools/discovery.py
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -481,7 +481,6 @@ def analyse_name(self):
         )
         if name:
             self.dist.metadata.name = name
-            self.dist.name = name
 
     def _find_name_single_package_or_module(self) -> Optional[str]:
         """Exactly one module or package"""


Test patch:
diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -508,6 +508,49 @@ def test_compatible_with_numpy_configuration(tmp_path):
     assert dist.packages is None
 
 
+def test_name_discovery_doesnt_break_cli(tmpdir_cwd):
+    jaraco.path.build({"pkg.py": ""})
+    dist = Distribution({})
+    dist.script_args = ["--name"]
+    dist.set_defaults()
+    dist.parse_command_line()  # <-- no exception should be raised here.
+    assert dist.get_name() == "pkg"
+
+
+def test_preserve_explicit_name_with_dynamic_version(tmpdir_cwd, monkeypatch):
+    """According to #3545 it seems that ``name`` discovery is running,
+    even when the project already explicitly sets it.
+    This seems to be related to parsing of dynamic versions (via ``attr`` directive),
+    which requires the auto-discovery of ``package_dir``.
+    """
+    files = {
+        "src": {
+            "pkg": {"__init__.py": "__version__ = 42\n"},
+        },
+        "pyproject.toml": DALS("""
+            [project]
+            name = "myproj"  # purposefully different from package name
+            dynamic = ["version"]
+            [tool.setuptools.dynamic]
+            version = {"attr" = "pkg.__version__"}
+            """)
+    }
+    jaraco.path.build(files)
+    dist = Distribution({})
+    orig_analyse_name = dist.set_defaults.analyse_name
+
+    def spy_analyse_name():
+        # We can check if name discovery was triggered by ensuring the original
+        # name remains instead of the package name.
+        orig_analyse_name()
+        assert dist.get_name() == "myproj"
+
+    monkeypatch.setattr(dist.set_defaults, "analyse_name", spy_analyse_name)
+    dist.parse_config_files()
+    assert dist.get_version() == "42"
+    assert set(dist.packages) == {"pkg"}
+
+
 def _populate_project_dir(root, files, options):
     # NOTE: Currently pypa/build will refuse to build the project if no
     # `pyproject.toml` or `setup.py` is found. So it is impossible to do