Repository: pypa/build
Pull request #260 (2021-03-18 18:04:27)

build: validate the source directory passed to ProjectBuilder

pfmoore: Fixes #259.

Initial version, for some reason the `tox -e fix` command doesn't work on my PC (it doesn't recognise my copy of Visual Studio when building the pre-commit environments) so I can't run the linting locally. I'll let CI do it for me...
pfmoore: I don't know why the tests are failing, they work locally.
FFY00: You seem to have found an issue with our integration tests :sweat_smile:. We test building a few popular projects in the CI as a sanity check and it looks like we are invoking `build` in the directory where the github tarballs are extracted, not the folder that is inside them and contains the project.

I will open a separate PR, you can then rebase on top of it.
FFY00: Btw, as the integration tests take a long time and are only included as a sanity check, you need to pass `--only-integration` or `--run-integration` to pytest. You can do this in tox as `tox -e py39 -- --only-integration`.

My PR will introduce a test dependency so you will need to recreate the tox environment, you can do so with `tox -r -e py39`

Sorry for the trouble!
gaborbernat: > I don't know why the tests are failing, they work locally.

This is why I'm not the biggest fan of not running integration tests locally by default 😆 People think if it passed locally they are done.
pfmoore: Yep, once the current tests pass I'll add tests for the new functionality.
FFY00: The tests should now be fixed now, @pfmoore can you rebase your branch on main?
pfmoore: Thanks for the quick fix, rebase done.
FFY00: Thanks, when you come back to add the tests, could you also add an entry in the changelog?
pfmoore: Done.

By the way, the symlink from `docs/changelog.rst` to `CHANGELOG.rst` doesn't get extracted properly on Windows, and messes up the git repo. Twice, I nearly uploaded a commit which changed that file to a plain text file containing the text `../CHANGELOG.rst` 🙁 

For portability, it might be better not to include symlinks in the repository...
FFY00: Uh, I thought symlinks were fixed on Windows, I guess that's not the case.
pfmoore: I do have symlinks enabled, and they do work. I suspect that the problem is that git might not handle them. I'm using a very vanilla git setup.


pfmoore: @gaborbernat I've raised #262 with details of the pre-commit failure.

@FFY00 I think I've done everything needed now. I've no idea why the coverage has gone down (or what I need to do to fix that). The coverage report seems to be flaggking lines that I haven't touched as uncovered 🙁 

Let me know what (if anything) more I need to do.
FFY00: Thanks! The coverage is a bit fiddly, it is fine now.

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 4f29848..f025405 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -3,6 +3,16 @@ Changelog
 +++++++++
 
 
+0.3.2 (XX-XX-2021)
+==================
+
+- Validate that the supplied source directory is valid (`PR #260`_, Fixes `#259`_)
+
+.. _PR #260: https://github.com/pypa/build/pull/260
+.. _#259: https://github.com/pypa/build/issues/259
+
+
+
 0.3.1 (09-03-2021)
 ==================
 
diff --git a/src/build/__init__.py b/src/build/__init__.py
index 1c4eb71..10e5c9d 100644
--- a/src/build/__init__.py
+++ b/src/build/__init__.py
@@ -59,6 +59,16 @@ class TypoWarning(Warning):
     """
 
 
+def _validate_source_directory(srcdir):
+    # type: (str) -> None
+    if not os.path.isdir(srcdir):
+        raise BuildException('Source {} is not a directory'.format(srcdir))
+    pyproject_toml = os.path.join(srcdir, 'pyproject.toml')
+    setup_py = os.path.join(srcdir, 'setup.py')
+    if not os.path.exists(pyproject_toml) and not os.path.exists(setup_py):
+        raise BuildException('Source {} does not appear to be a Python project: no pyproject.toml or setup.py'.format(srcdir))
+
+
 def check_dependency(req_string, ancestral_req_strings=(), parent_extras=frozenset()):
     # type: (str, Tuple[str, ...], AbstractSet[str]) -> Iterator[Tuple[str, ...]]
     """
@@ -137,6 +147,7 @@ class ProjectBuilder(object):
         :param python_executable: The python executable where the backend lives
         """
         self.srcdir = os.path.abspath(srcdir)  # type: str
+        _validate_source_directory(srcdir)
 
         spec_file = os.path.join(srcdir, 'pyproject.toml')
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index ee7a496..e04914c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -135,6 +135,11 @@ def test_no_backend_path(packages_path):
     return os.path.join(packages_path, 'test-no-backend')
 
 
+@pytest.fixture
+def test_no_project_path(packages_path):
+    return os.path.join(packages_path, 'test-no-project')
+
+
 @pytest.fixture
 def test_no_requires_path(packages_path):
     return os.path.join(packages_path, 'test-no-requires')
diff --git a/tests/packages/test-no-project/empty.txt b/tests/packages/test-no-project/empty.txt
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_projectbuilder.py b/tests/test_projectbuilder.py
index c83895c..1485b74 100644
--- a/tests/test_projectbuilder.py
+++ b/tests/test_projectbuilder.py
@@ -136,6 +136,18 @@ def test_check_dependency(monkeypatch, requirement_string, expected):
     assert next(build.check_dependency(requirement_string), None) == expected
 
 
+def test_bad_project(test_no_project_path):
+    # Passing a nonexistent project directory
+    with pytest.raises(build.BuildException):
+        build.ProjectBuilder(os.path.join(test_no_project_path, 'does-not-exist'))
+    # Passing a file as a project directory
+    with pytest.raises(build.BuildException):
+        build.ProjectBuilder(os.path.join(test_no_project_path, 'empty.txt'))
+    # Passing a project directory with no pyproject.toml or setup.py
+    with pytest.raises(build.BuildException):
+        build.ProjectBuilder(test_no_project_path)
+
+
 def test_init(mocker, test_flit_path, legacy_path, test_no_permission, test_bad_syntax_path):
     mocker.patch('pep517.wrappers.Pep517HookCaller')