diff --git a/docs/docs/cli.md b/docs/docs/cli.md index 64cf17941e5..af7c8b1a81a 100644 --- a/docs/docs/cli.md +++ b/docs/docs/cli.md @@ -31,12 +31,11 @@ will create a folder as follows: ```text my-package ├── pyproject.toml -├── README.rst +├── README.md ├── my_package │ └── __init__.py └── tests - ├── __init__.py - └── test_my_package.py + └── __init__.py ``` If you want to name your project differently than the folder, you can pass @@ -57,13 +56,33 @@ That will create a folder structure as follows: ```text my-package ├── pyproject.toml -├── README.rst +├── README.md ├── src │ └── my_package │ └── __init__.py └── tests - ├── __init__.py - └── test_my_package.py + └── __init__.py +``` + +The `--name` option is smart enough to detect namespace packages and create +the required structure for you. + +```bash +poetry new --src --name my.package my-package +``` + +will create the following structure: + +```text +my-package +├── pyproject.toml +├── README.md +├── src +│ └── my +│ └── package +│ └── __init__.py +└── tests + └── __init__.py ``` ## init diff --git a/poetry/console/commands/new.py b/poetry/console/commands/new.py index 04c3a8ba653..8158442f741 100644 --- a/poetry/console/commands/new.py +++ b/poetry/console/commands/new.py @@ -3,8 +3,6 @@ from cleo.helpers import argument from cleo.helpers import option -from poetry.utils.helpers import module_name - from .command import Command @@ -17,12 +15,17 @@ class NewCommand(Command): options = [ option("name", None, "Set the resulting package name.", flag=False), option("src", None, "Use the src layout for the project."), + option( + "readme", + None, + "Specify the readme file format. One of md (default) or rst", + flag=False, + ), ] def handle(self) -> None: from pathlib import Path - from poetry.core.semver.helpers import parse_constraint from poetry.core.vcs.git import GitConfig from poetry.layouts import layout from poetry.utils.env import SystemEnv @@ -32,7 +35,11 @@ def handle(self) -> None: else: layout_ = layout("standard") - path = Path.cwd() / Path(self.argument("path")) + path = Path(self.argument("path")) + if not path.is_absolute(): + # we do not use resolve here due to compatibility issues for path.resolve(strict=False) + path = Path.cwd().joinpath(path) + name = self.option("name") if not name: name = path.name @@ -45,7 +52,7 @@ def handle(self) -> None: "exists and is not empty".format(path) ) - readme_format = "rst" + readme_format = self.option("readme") or "md" config = GitConfig() author = None @@ -60,25 +67,24 @@ def handle(self) -> None: ".".join(str(v) for v in current_env.version_info[:2]) ) - dev_dependencies = {} - python_constraint = parse_constraint(default_python) - if parse_constraint("<3.5").allows_any(python_constraint): - dev_dependencies["pytest"] = "^4.6" - if parse_constraint(">=3.5").allows_all(python_constraint): - dev_dependencies["pytest"] = "^5.2" - layout_ = layout_( name, "0.1.0", author=author, readme_format=readme_format, python=default_python, - dev_dependencies=dev_dependencies, ) layout_.create(path) + path = path.resolve() + + try: + path = path.relative_to(Path.cwd()) + except ValueError: + pass + self.line( "Created package {} in {}".format( - module_name(name), path.relative_to(Path.cwd()) + layout_._package_name, path.as_posix() # noqa ) ) diff --git a/poetry/layouts/__init__.py b/poetry/layouts/__init__.py index 291319e82c1..f7ddf8c4def 100644 --- a/poetry/layouts/__init__.py +++ b/poetry/layouts/__init__.py @@ -2,10 +2,9 @@ from .layout import Layout from .src import SrcLayout -from .standard import StandardLayout -_LAYOUTS = {"src": SrcLayout, "standard": StandardLayout} +_LAYOUTS = {"src": SrcLayout, "standard": Layout} def layout(name: str) -> Type[Layout]: diff --git a/poetry/layouts/layout.py b/poetry/layouts/layout.py index dfc2e609c95..f4610564582 100644 --- a/poetry/layouts/layout.py +++ b/poetry/layouts/layout.py @@ -1,26 +1,22 @@ +from pathlib import Path from typing import TYPE_CHECKING from typing import Dict from typing import Optional from tomlkit import dumps +from tomlkit import inline_table from tomlkit import loads from tomlkit import table +from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import module_name if TYPE_CHECKING: - from pathlib import Path + from tomlkit.items import InlineTable from poetry.core.pyproject.toml import PyProjectTOML -TESTS_DEFAULT = """from {package_name} import __version__ - - -def test_version(): - assert __version__ == '{version}' -""" - POETRY_DEFAULT = """\ [tool.poetry] @@ -28,30 +24,22 @@ def test_version(): version = "" description = "" authors = [] - -[tool.poetry.dependencies] - -[tool.poetry.dev-dependencies] -""" - -POETRY_WITH_LICENSE = """\ -[tool.poetry] -name = "" -version = "" -description = "" -authors = [] license = "" +readme = "" +packages = [] [tool.poetry.dependencies] [tool.poetry.dev-dependencies] """ -BUILD_SYSTEM_MIN_VERSION = "1.0.0" +BUILD_SYSTEM_MIN_VERSION: Optional[str] = None BUILD_SYSTEM_MAX_VERSION: Optional[str] = None class Layout: + ACCEPTED_README_FORMATS = {"md", "rst"} + def __init__( self, project: str, @@ -59,16 +47,27 @@ def __init__( description: str = "", readme_format: str = "md", author: Optional[str] = None, - license: Optional[str] = None, + license: Optional[str] = None, # noqa python: str = "*", dependencies: Optional[Dict[str, str]] = None, dev_dependencies: Optional[Dict[str, str]] = None, ): - self._project = project - self._package_name = module_name(project) + self._project = canonicalize_name(project).replace(".", "-") + self._package_path_relative = Path( + *(module_name(part) for part in canonicalize_name(project).split(".")) + ) + self._package_name = ".".join(self._package_path_relative.parts) self._version = version self._description = description - self._readme_format = readme_format + + self._readme_format = readme_format.lower() + if self._readme_format not in self.ACCEPTED_README_FORMATS: + raise ValueError( + "Invalid readme format '{}', use one of {}.".format( + readme_format, ", ".join(self.ACCEPTED_README_FORMATS) + ) + ) + self._license = license self._python = python self._dependencies = dependencies or {} @@ -79,6 +78,30 @@ def __init__( self._author = author + @property + def basedir(self) -> Path: + return Path() + + @property + def package_path(self) -> Path: + return self.basedir / self._package_path_relative + + def get_package_include(self) -> Optional["InlineTable"]: + package = inline_table() + + include = self._package_path_relative.parts[0] + package.append("include", include) + + if self.basedir != Path(): + package.append("from", self.basedir.as_posix()) + else: + if include == self._project: + # package include and package name are the same, + # packages table is redundant here. + return None + + return package + def create(self, path: "Path", with_tests: bool = True) -> None: path.mkdir(parents=True, exist_ok=True) @@ -94,17 +117,26 @@ def generate_poetry_content( self, original: Optional["PyProjectTOML"] = None ) -> str: template = POETRY_DEFAULT - if self._license: - template = POETRY_WITH_LICENSE content = loads(template) + poetry_content = content["tool"]["poetry"] poetry_content["name"] = self._project poetry_content["version"] = self._version poetry_content["description"] = self._description poetry_content["authors"].append(self._author) + if self._license: poetry_content["license"] = self._license + else: + poetry_content.remove("license") + + poetry_content["readme"] = "README.{}".format(self._readme_format) + packages = self.get_package_include() + if packages: + poetry_content["packages"].append(packages) + else: + poetry_content.remove("packages") poetry_content["dependencies"]["python"] = self._python @@ -116,9 +148,14 @@ def generate_poetry_content( # Add build system build_system = table() - build_system_version = ">=" + BUILD_SYSTEM_MIN_VERSION + build_system_version = "" + + if BUILD_SYSTEM_MIN_VERSION is not None: + build_system_version = ">=" + BUILD_SYSTEM_MIN_VERSION if BUILD_SYSTEM_MAX_VERSION is not None: - build_system_version += ",<" + BUILD_SYSTEM_MAX_VERSION + if build_system_version: + build_system_version += "," + build_system_version += "<" + BUILD_SYSTEM_MAX_VERSION build_system.add("requires", ["poetry-core" + build_system_version]) build_system.add("build-backend", "poetry.core.masonry.api") @@ -133,30 +170,24 @@ def generate_poetry_content( return content def _create_default(self, path: "Path", src: bool = True) -> None: - raise NotImplementedError() + package_path = path / self.package_path + package_path.mkdir(parents=True) - def _create_readme(self, path: "Path") -> None: - if self._readme_format == "rst": - readme_file = path / "README.rst" - else: - readme_file = path / "README.md" + package_init = package_path / "__init__.py" + package_init.touch() + def _create_readme(self, path: "Path") -> "Path": + readme_file = path.joinpath("README.{}".format(self._readme_format)) readme_file.touch() + return readme_file - def _create_tests(self, path: "Path") -> None: + @staticmethod + def _create_tests(path: "Path") -> None: tests = path / "tests" - tests_init = tests / "__init__.py" - tests_default = tests / f"test_{self._package_name}.py" - tests.mkdir() - tests_init.touch(exist_ok=False) - with tests_default.open("w", encoding="utf-8") as f: - f.write( - TESTS_DEFAULT.format( - package_name=self._package_name, version=self._version - ) - ) + tests_init = tests / "__init__.py" + tests_init.touch(exist_ok=False) def _write_poetry(self, path: "Path") -> None: content = self.generate_poetry_content() diff --git a/poetry/layouts/src.py b/poetry/layouts/src.py index 4f43263ef93..6d10e63296b 100644 --- a/poetry/layouts/src.py +++ b/poetry/layouts/src.py @@ -1,22 +1,9 @@ -from typing import TYPE_CHECKING +from pathlib import Path from .layout import Layout -if TYPE_CHECKING: - from pathlib import Path - -DEFAULT = """__version__ = '{version}' -""" - - class SrcLayout(Layout): - def _create_default(self, path: "Path") -> None: - package_path = path / "src" / self._package_name - - package_init = package_path / "__init__.py" - - package_path.mkdir(parents=True) - - with package_init.open("w", encoding="utf-8") as f: - f.write(DEFAULT.format(version=self._version)) + @property + def basedir(self) -> "Path": + return Path("src") diff --git a/poetry/layouts/standard.py b/poetry/layouts/standard.py index 9971d4aed8d..e69de29bb2d 100644 --- a/poetry/layouts/standard.py +++ b/poetry/layouts/standard.py @@ -1,21 +0,0 @@ -from typing import TYPE_CHECKING - -from .layout import Layout - - -if TYPE_CHECKING: - from pathlib import Path -DEFAULT = """__version__ = '{version}' -""" - - -class StandardLayout(Layout): - def _create_default(self, path: "Path") -> None: - package_path = path / self._package_name - - package_init = package_path / "__init__.py" - - package_path.mkdir() - - with package_init.open("w", encoding="utf-8") as f: - f.write(DEFAULT.format(version=self._version)) diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py index 087c3f2088b..4ae13465399 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -66,6 +66,8 @@ def init_basic_toml(): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -112,6 +114,8 @@ def test_interactive_with_dependencies(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -144,6 +148,8 @@ def test_empty_license(tester): version = "1.2.3" description = "" authors = ["Your Name "] +readme = "README.md" +packages = [{{include = "my_package"}}] [tool.poetry.dependencies] python = "^{python}" @@ -152,7 +158,6 @@ def test_empty_license(tester): """.format( python=".".join(str(c) for c in sys.version_info[:2]) ) - assert expected in tester.io.fetch_output() @@ -186,6 +191,8 @@ def test_interactive_with_git_dependencies(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -228,6 +235,8 @@ def test_interactive_with_git_dependencies_with_reference(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -270,6 +279,8 @@ def test_interactive_with_git_dependencies_and_other_name(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -315,6 +326,8 @@ def test_interactive_with_directory_dependency(tester, repo, source_dir, fixture description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -361,6 +374,8 @@ def test_interactive_with_directory_dependency_and_other_name( description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -406,6 +421,8 @@ def test_interactive_with_file_dependency(tester, repo, source_dir, fixture_dir) description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -438,6 +455,8 @@ def test_python_option(tester): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -471,6 +490,8 @@ def test_predefined_dependency(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -511,6 +532,8 @@ def test_predefined_and_interactive_dependencies(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -545,6 +568,8 @@ def test_predefined_dev_dependency(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" @@ -585,11 +610,15 @@ def test_predefined_and_interactive_dev_dependencies(tester, repo): description = "This is a description" authors = ["Your Name "] license = "MIT" +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "~2.7 || ^3.6" [tool.poetry.dev-dependencies] +pytest = "^3.6.0" +pytest-requests = "^0.2.0" """ output = tester.io.fetch_output() @@ -648,6 +677,8 @@ def test_init_non_interactive_existing_pyproject_add_dependency( version = "0.1.0" description = "" authors = ["Your Name "] +readme = "README.md" +packages = [{include = "my_package"}] [tool.poetry.dependencies] python = "^3.6" diff --git a/tests/console/commands/test_new.py b/tests/console/commands/test_new.py new file mode 100644 index 00000000000..0df5149ae0c --- /dev/null +++ b/tests/console/commands/test_new.py @@ -0,0 +1,155 @@ +from pathlib import Path +from typing import Optional + +import pytest + +from poetry.factory import Factory +from poetry.poetry import Poetry + + +@pytest.fixture +def tester(command_tester_factory): + return command_tester_factory("new") + + +def verify_project_directory( + path: Path, package_name: str, package_path: str, include_from: Optional[str] = None +) -> Poetry: + package_path = Path(package_path) + assert path.is_dir() + + pyproject = path / "pyproject.toml" + assert pyproject.is_file() + + init_file = path / package_path / "__init__.py" + assert init_file.is_file() + + tests_init_file = path / "tests" / "__init__.py" + assert tests_init_file.is_file() + + poetry = Factory().create_poetry(cwd=path) + assert poetry.package.name == package_name + + if include_from: + package_include = { + "include": package_path.relative_to(include_from).parts[0], + "from": include_from, + } + else: + package_include = {"include": package_path.parts[0]} + + packages = poetry.local_config.get("packages") + + if not packages: + assert poetry.local_config.get("name") == package_include.get("include") + else: + assert len(packages) == 1 + assert packages[0] == package_include + + return poetry + + +@pytest.mark.parametrize( + "options,directory,package_name,package_path,include_from", + [ + ([], "package", "package", "package", None), + (["--src"], "package", "package", "src/package", "src"), + ( + ["--name namespace.package"], + "namespace-package", + "namespace-package", + "namespace/package", + None, + ), + ( + ["--src", "--name namespace.package"], + "namespace-package", + "namespace-package", + "src/namespace/package", + "src", + ), + ( + ["--name namespace.package_a"], + "namespace-package_a", + "namespace-package-a", + "namespace/package_a", + None, + ), + ( + ["--src", "--name namespace.package_a"], + "namespace-package_a", + "namespace-package-a", + "src/namespace/package_a", + "src", + ), + ( + ["--name namespace_package"], + "namespace-package", + "namespace-package", + "namespace_package", + None, + ), + ( + ["--name namespace_package", "--src"], + "namespace-package", + "namespace-package", + "src/namespace_package", + "src", + ), + ( + ["--name namespace.package"], + "package", + "namespace-package", + "namespace/package", + None, + ), + ( + ["--name namespace.package", "--src"], + "package", + "namespace-package", + "src/namespace/package", + "src", + ), + ( + ["--name namespace.package"], + "package", + "namespace-package", + "namespace/package", + None, + ), + ( + ["--name namespace.package", "--src"], + "package", + "namespace-package", + "src/namespace/package", + "src", + ), + ([], "namespace_package", "namespace-package", "namespace_package", None), + ( + ["--src", "--name namespace_package"], + "namespace_package", + "namespace-package", + "src/namespace_package", + "src", + ), + ], +) +def test_command_new( + options, directory, package_name, package_path, include_from, tester, tmp_dir +): + path = Path(tmp_dir) / directory + options.append(path.as_posix()) + tester.execute(" ".join(options)) + verify_project_directory(path, package_name, package_path, include_from) + + +@pytest.mark.parametrize("fmt", [(None,), ("md",), ("rst",)]) +def test_command_new_with_readme(fmt, tester, tmp_dir): + fmt = "md" + package = "package" + path = Path(tmp_dir) / package + options = ["--readme {}".format(fmt) if fmt else "md", path.as_posix()] + tester.execute(" ".join(options)) + + poetry = verify_project_directory(path, package, package, None) + assert poetry.local_config.get("readme") == "README.{}".format(fmt or "md")