Commit 4113773a by Sébastien Eustace

Add a new init command

parent e552d9ab
......@@ -5,6 +5,7 @@
### Added
- Added a new, more efficient dependency resolver.
- Added a new `init` command to generate a `pyproject.toml` file in existing projects.
- Added the `--extras` and `--python` options to `debug:resolve` to help debug dependency resolution.
### Changed
......
......@@ -264,6 +264,24 @@ the `--name` option:
poetry new my-folder --name my-package
```
### init
This command will help you create a `pyproject.toml` file interactively
by prompting you to provide basic information about your package.
It will interactively ask you to fill in the fields, while using some smart defaults.
```bash
poetry init
```
#### Options
* `--name`: Name of the package.
* `--description`: Description of the package.
* `--author`: Author of the package.
* `--dependency`: Package to require with a version constraint. Should be in format `foo:1.0.0`.
* `--dev-dependency`: Development requirements, see `--require`.
### install
......
......@@ -54,6 +54,26 @@ the `--name` option:
poetry new my-folder --name my-package
```
## init
This command will help you create a `pyproject.toml` file interactively
by prompting you to provide basic information about your package.
It will interactively ask you to fill in the fields, while using some smart defaults.
```bash
poetry init
```
### Options
* `--name`: Name of the package.
* `--description`: Description of the package.
* `--author`: Author of the package.
* `--dependency`: Package to require with a version constraint. Should be in format `foo:1.0.0`.
* `--dev-dependency`: Development requirements, see `--require`.
## install
The `install` command reads the `pyproject.toml` file from the current project,
......
......@@ -18,6 +18,7 @@ from .commands import AddCommand
from .commands import BuildCommand
from .commands import CheckCommand
from .commands import ConfigCommand
from .commands import InitCommand
from .commands import InstallCommand
from .commands import LockCommand
from .commands import NewCommand
......@@ -106,6 +107,7 @@ class Application(BaseApplication):
BuildCommand(),
CheckCommand(),
ConfigCommand(),
InitCommand(),
InstallCommand(),
LockCommand(),
NewCommand(),
......
......@@ -3,6 +3,7 @@ from .add import AddCommand
from .build import BuildCommand
from .check import CheckCommand
from .config import ConfigCommand
from .init import InitCommand
from .install import InstallCommand
from .lock import LockCommand
from .new import NewCommand
......
import re
from typing import List
from typing import Tuple
from .init import InitCommand
from .venv_command import VenvCommand
class AddCommand(VenvCommand):
class AddCommand(VenvCommand, InitCommand):
"""
Add a new dependency to <comment>pyproject.toml</>.
......@@ -147,94 +143,3 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
self.poetry.file.write(original_content)
return status
def _determine_requirements(self,
requires, # type: List[str]
allow_prereleases=False, # type: bool
): # type: (...) -> List[str]
if not requires:
return []
requires = self._parse_name_version_pairs(requires)
result = []
for requirement in requires:
if 'version' not in requirement:
# determine the best version automatically
name, version = self._find_best_version_for_package(
requirement['name'],
allow_prereleases=allow_prereleases
)
requirement['version'] = version
requirement['name'] = name
self.line(
'Using version <info>{}</> for <info>{}</>'
.format(version, name)
)
else:
# check that the specified version/constraint exists
# before we proceed
name, _ = self._find_best_version_for_package(
requirement['name'], requirement['version'],
allow_prereleases=allow_prereleases
)
requirement['name'] = name
result.append(
'{} {}'.format(requirement['name'], requirement['version'])
)
return result
def _find_best_version_for_package(self,
name,
required_version=None,
allow_prereleases=False
): # type: (...) -> Tuple[str, str]
from poetry.version.version_selector import VersionSelector
selector = VersionSelector(self.poetry.pool)
package = selector.find_best_candidate(
name, required_version,
allow_prereleases=allow_prereleases
)
if not package:
# TODO: find similar
raise ValueError(
'Could not find a matching version of package {}'.format(name)
)
return (
package.pretty_name,
selector.find_recommended_require_version(package)
)
def _parse_name_version_pairs(self, pairs): # type: (list) -> list
result = []
for i in range(len(pairs)):
pair = re.sub('^([^=: ]+)[=: ](.*)$', '\\1 \\2', pairs[i].strip())
pair = pair.strip()
if ' ' in pair:
name, version = pair.split(' ', 2)
result.append({
'name': name,
'version': version
})
else:
result.append({
'name': pair
})
return result
def _format_requirements(self, requirements): # type: (List[str]) -> dict
requires = {}
requirements = self._parse_name_version_pairs(requirements)
for requirement in requirements:
requires[requirement['name']] = requirement['version']
return requires
import re
from typing import List
from typing import Tuple
from .command import Command
class InitCommand(Command):
"""
Creates a basic <comment>pyproject.toml</> file in the current directory.
init
{--name= : Name of the package}
{--description= : Description of the package}
{--author= : Author name of the package}
{--dependency=* : Package to require with an optional version constraint,
e.g. requests:^2.10.0 or requests=2.11.1}
{--dev-dependency=* : Package to require for development with an optional version constraint,
e.g. requests:^2.10.0 or requests=2.11.1}
{--l|license= : License of the package}
"""
help = """\
The <info>init</info> command creates a basic <comment>pyproject.toml</> file in the current directory.
"""
def __init__(self):
super(InitCommand, self).__init__()
self.pool = None
def handle(self):
from poetry.layouts import layout
from poetry.utils._compat import Path
from poetry.vcs.git import GitConfig
if (Path.cwd() / 'pyproject.toml').exists():
self.error('A pyproject.toml file already exists.')
return 1
vcs_config = GitConfig()
self.line([
'',
'This command will guide you through creating your <info>poetry.toml</> config.',
''
])
name = self.option('name')
if not name:
name = Path.cwd().name.lower()
question = self.create_question(
'Package name [<comment>{}</comment>]: '.format(name),
default=name
)
name = self.ask(question)
version = '0.1.0'
question = self.create_question(
'Version [<comment>{}</comment>]: '.format(version),
default=version
)
version = self.ask(question)
description = self.option('description') or ''
question = self.create_question(
'Description [<comment>{}</comment>]: '.format(description),
default=description
)
description = self.ask(question)
author = self.option('author')
if not author and vcs_config and vcs_config.get('user.name'):
author = vcs_config['user.name']
author_email = vcs_config.get('user.email')
if author_email:
author += ' <{}>'.format(author_email)
question = self.create_question(
'Author [<comment>{}</comment>, n to skip]: '.format(author),
default=author
)
question.validator = lambda v: self._validate_author(v, author)
author = self.ask(question)
if not author:
authors = []
else:
authors = [author]
license = self.option('license') or ''
question = self.create_question(
'License [<comment>{}</comment>]: '.format(license),
default=license
)
license = self.ask(question)
question = self.create_question(
'Compatible Python versions [*]: ',
default='*'
)
python = self.ask(question)
self.line('')
requirements = []
question = 'Would you like to define your dependencies' \
' (require) interactively?'
if self.confirm(question, True):
requirements = self._format_requirements(
self._determine_requirements(self.option('dependency'))
)
dev_requirements = []
question = 'Would you like to define your dev dependencies' \
' (require-dev) interactively'
if self.confirm(question, True):
dev_requirements = self._format_requirements(
self._determine_requirements(self.option('dev-dependency'))
)
layout_ = layout('standard')(
name,
version,
description=description,
author=authors[0] if authors else None,
license=license,
python=python,
dependencies=requirements,
dev_dependencies=dev_requirements
)
content = layout_.generate_poetry_content()
if self.input.is_interactive():
self.line('<info>Generated file</info>')
self.line(['', content, ''])
if not self.confirm('Do you confirm generation?', True):
self.line('<error>Command aborted</error>')
return 1
with (Path.cwd() / 'pyproject.toml').open('w') as f:
f.write(content)
def _determine_requirements(self,
requires, # type: List[str]
allow_prereleases=False, # type: bool
): # type: (...) -> List[str]
if not requires:
requires = []
package = self.ask('Search for package:')
while package is not None:
matches = self._get_pool().search(package)
if not matches:
self.line('<error>Unable to find package</error>')
package = False
else:
choices = []
for found_package in matches:
choices.append(found_package.pretty_name)
self.line(
'Found <info>{}</info> packages matching <info>{}</info>'
.format(
len(matches),
package
)
)
package = self.choice(
'\nEnter package # to add, or the complete package name if it is not listed',
choices,
attempts=3
)
# no constraint yet, determine the best version automatically
if package is not False and ' ' not in package:
question = self.create_question(
'Enter the version constraint to require '
'(or leave blank to use the latest version):'
)
question.attempts = 3
question.validator = lambda x: (x or '').strip() or False
constraint = self.ask(question)
if constraint is False:
_, constraint = self._find_best_version_for_package(package)
self.line(
'Using version <info>{}</info> for <info>{}</info>'
.format(constraint, package)
)
package += ' {}'.format(constraint)
if package is not False:
requires.append(package)
package = self.ask('\nSearch for a package:')
return requires
requires = self._parse_name_version_pairs(requires)
result = []
for requirement in requires:
if 'version' not in requirement:
# determine the best version automatically
name, version = self._find_best_version_for_package(
requirement['name'],
allow_prereleases=allow_prereleases
)
requirement['version'] = version
requirement['name'] = name
self.line(
'Using version <info>{}</> for <info>{}</>'
.format(version, name)
)
else:
# check that the specified version/constraint exists
# before we proceed
name, _ = self._find_best_version_for_package(
requirement['name'], requirement['version'],
allow_prereleases=allow_prereleases
)
requirement['name'] = name
result.append(
'{} {}'.format(requirement['name'], requirement['version'])
)
return result
def _find_best_version_for_package(self,
name,
required_version=None,
allow_prereleases=False
): # type: (...) -> Tuple[str, str]
from poetry.version.version_selector import VersionSelector
selector = VersionSelector(self._get_pool())
package = selector.find_best_candidate(
name, required_version,
allow_prereleases=allow_prereleases
)
if not package:
# TODO: find similar
raise ValueError(
'Could not find a matching version of package {}'.format(name)
)
return (
package.pretty_name,
selector.find_recommended_require_version(package)
)
def _parse_name_version_pairs(self, pairs): # type: (list) -> list
result = []
for i in range(len(pairs)):
pair = re.sub('^([^=: ]+)[=: ](.*)$', '\\1 \\2', pairs[i].strip())
pair = pair.strip()
if ' ' in pair:
name, version = pair.split(' ', 2)
result.append({
'name': name,
'version': version
})
else:
result.append({
'name': pair
})
return result
def _format_requirements(self, requirements): # type: (List[str]) -> dict
requires = {}
requirements = self._parse_name_version_pairs(requirements)
for requirement in requirements:
requires[requirement['name']] = requirement['version']
return requires
def _validate_author(self, author, default):
from poetry.packages.package import AUTHOR_REGEX
author = author or default
if author in ['n', 'no']:
return
m = AUTHOR_REGEX.match(author)
if not m:
raise ValueError(
'Invalid author string. Must be in the format: '
'John Smith <john@example.com>'
)
return author
def _get_pool(self):
if self.pool is None:
self.pool = self.poetry.pool
return self.pool
from poetry.utils._compat import Path
from .command import Command
......@@ -14,6 +12,8 @@ class NewCommand(Command):
def handle(self):
from poetry.layouts import layout
from poetry.utils._compat import Path
from poetry.vcs.git import GitConfig
layout_ = layout('standard')
......@@ -34,7 +34,15 @@ class NewCommand(Command):
readme_format = 'rst'
layout_ = layout_(name, '0.1.0', readme_format=readme_format)
config = GitConfig()
author = None
if config.get('user.name'):
author = config['user.name']
author_email = config.get('user.email')
if author_email:
author += ' <{}>'.format(author_email)
layout_ = layout_(name, '0.1.0', author=author, readme_format=readme_format)
layout_.create(path)
self.line(
......
......@@ -9,7 +9,7 @@ class PoetryStyle(CleoStyle):
self.output.get_formatter().add_style('error', 'red')
self.output.get_formatter().add_style('warning', 'yellow')
self.output.get_formatter().add_style('question', 'blue')
self.output.get_formatter().add_style('question', 'cyan')
self.output.get_formatter().add_style('comment', 'blue')
def writeln(self, messages,
......
from poetry.toml import dumps
from poetry.toml import loads
from poetry.utils.helpers import module_name
from poetry.vcs.git import Git
TESTS_DEFAULT = u"""from {package_name} import __version__
......@@ -20,45 +19,52 @@ description = ""
authors = []
[tool.poetry.dependencies]
python = "*"
[tool.poetry.dev-dependencies]
pytest = "^3.5"
"""
POETRY_WITH_LICENSE = """\
[tool.poetry]
name = ""
version = ""
description = ""
authors = []
license = ""
[tool.poetry.dependencies]
[tool.poetry.dev-dependencies]
"""
class Layout(object):
def __init__(self, project, version='0.1.0', readme_format='md', author=None):
def __init__(self,
project,
version='0.1.0',
description='',
readme_format='md',
author=None,
license=None,
python='*',
dependencies=None,
dev_dependencies=None):
self._project = project
self._package_name = module_name(project)
self._version = version
self._description = description
self._readme_format = readme_format
self._dependencies = {}
self._dev_dependencies = {}
self._include = []
self._license = license
self._python = python
self._dependencies = dependencies or {}
self._dev_dependencies = dev_dependencies or {'pytest': '^3.5'}
self._git = Git()
git_config = self._git.config
if not author:
if (
git_config.get('user.name')
and git_config.get('user.email')
):
author = u'{} <{}>'.format(
git_config['user.name'],
git_config['user.email']
)
else:
author = 'Your Name <you@example.com>'
author = 'Your Name <you@example.com>'
self._author = author
def create(self, path, with_tests=True):
self._dependencies = {}
self._dev_dependencies = {}
self._include = []
path.mkdir(parents=True, exist_ok=True)
self._create_default(path)
......@@ -69,6 +75,30 @@ class Layout(object):
self._write_poetry(path)
def generate_poetry_content(self):
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
poetry_content['dependencies']['python'] = self._python
for dep_name, dep_constraint in self._dependencies.items():
poetry_content['dependencies'][dep_name] = dep_constraint
for dep_name, dep_constraint in self._dev_dependencies.items():
poetry_content['dev-dependencies'][dep_name] = dep_constraint
return dumps(content)
def _create_default(self, path, src=True):
raise NotImplementedError()
......@@ -99,13 +129,9 @@ class Layout(object):
)
def _write_poetry(self, path):
content = loads(POETRY_DEFAULT)
poetry_content = content['tool']['poetry']
poetry_content['name'] = self._project
poetry_content['version'] = self._version
poetry_content['authors'].append(self._author)
content = self.generate_poetry_content()
poetry = path / 'pyproject.toml'
with poetry.open('w') as f:
f.write(dumps(content))
f.write(content)
......@@ -80,5 +80,14 @@ class Repository(BaseRepository):
if index is not None:
del self._packages[index]
def search(self, query, mode=0):
results = []
for package in self.packages:
if query in package.name:
results.append(package)
return results
def __len__(self):
return len(self._packages)
......@@ -9,17 +9,20 @@ from poetry.utils._compat import decode
class GitConfig:
def __init__(self):
config_list = decode(subprocess.check_output(
['git', 'config', '-l'],
stderr=subprocess.STDOUT
))
self._config = {}
m = re.findall('(?ms)^([^=]+)=(.*?)$', config_list)
if m:
for group in m:
self._config[group[0]] = group[1]
try:
config_list = decode(subprocess.check_output(
['git', 'config', '-l'],
stderr=subprocess.STDOUT
))
m = re.findall('(?ms)^([^=]+)=(.*?)$', config_list)
if m:
for group in m:
self._config[group[0]] = group[1]
except subprocess.CalledProcessError:
pass
def get(self, key, default=None):
return self._config.get(key, default)
......
......@@ -23,7 +23,7 @@ classifiers = [
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.4"
cleo = "^0.6"
cleo = "^0.6.6"
requests = "^2.18"
toml = "^0.9"
cachy = "^0.2"
......
import pytest
import shutil
import tempfile
from cleo.testers import CommandTester
from poetry.utils._compat import Path
from tests.helpers import get_package
@pytest.fixture
def tmp_dir():
dir_ = tempfile.mkdtemp(prefix='poetry_')
yield dir_
shutil.rmtree(dir_)
def test_basic_interactive(app, mocker):
command = app.find('init')
mocker.patch('poetry.utils._compat.Path.open')
p = mocker.patch('poetry.utils._compat.Path.cwd')
p.return_value = Path(__file__)
tester = CommandTester(command)
tester.set_inputs([
'my-package', # Package name
'1.2.3', # Version
'This is a description', # Description
'n', # Author
'MIT', # License
'~2.7 || ^3.6', # Python
'n', # Interactive packages
'n', # Interactive dev packages
'\n' # Generate
])
tester.execute([('command', command.name)])
output = tester.get_display()
expected = """\
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "This is a description"
authors = ["Your Name <you@example.com>"]
license = "MIT"
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
[tool.poetry.dev-dependencies]
pytest = "^3.5"
"""
assert expected in output
def test_interactive_with_dependencies(app, repo, mocker):
repo.add_package(get_package('pendulum', '2.0.0'))
command = app.find('init')
mocker.patch('poetry.utils._compat.Path.open')
p = mocker.patch('poetry.utils._compat.Path.cwd')
p.return_value = Path(__file__).parent
tester = CommandTester(command)
tester.set_inputs([
'my-package', # Package name
'1.2.3', # Version
'This is a description', # Description
'n', # Author
'MIT', # License
'~2.7 || ^3.6', # Python
'', # Interactive packages
'pendulum', # Search for package
'0', # First option
'', # Do not set constraint
'', # Stop searching for packages
'n', # Interactive dev packages
'\n' # Generate
])
tester.execute([('command', command.name)])
output = tester.get_display()
expected = """\
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "This is a description"
authors = ["Your Name <you@example.com>"]
license = "MIT"
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
pendulum = "^2.0"
[tool.poetry.dev-dependencies]
pytest = "^3.5"
"""
print(output)
assert expected in output
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment