`poetry add` with `||` operator breaks further poetry commands
Description
I am able to add a dependency like this : poetry add 'pytest=^4|^6. However if I run poetry install or similar command that needs to read the pyproject.toml, I see this error:
The requirement is invalid: Unexpected character at column 16
pytest (>=4,<5 || >=6,<7)
Similarly, if I try to add the dependency this this: poetry add 'pytest>=4,<7,!=5.*. I get a similar error when I run something like poetry install:
The requirement is invalid: Unexpected character at column 21
pytest (>=4,<5.dev0 || ==6.*)
Workarounds
- Manually remove the dependency from
pyproject.toml - Run
poetry update - Manually add this:
[tool.poetry.dependencies] pytest = "^4|^6" - Run
poetry update
Poetry Installation Method
pip inside a virtualenv
Operating System
Ubuntu 24.04
Poetry Version
2.2.1
Poetry Configuration
cache-dir = "/home/rzuckerm/.cache/pypoetry"
data-dir = "/home/rzuckerm/.local/share/pypoetry"
installer.max-workers = null
installer.no-binary = null
installer.only-binary = null
installer.parallel = true
installer.re-resolve = true
keyring.enabled = true
python.installation-dir = "{data-dir}/python" # /home/rzuckerm/.local/share/pypoetry/python
requests.max-retries = 0
solver.lazy-wheel = true
system-git-client = false
virtualenvs.create = true
virtualenvs.in-project = null
virtualenvs.options.always-copy = false
virtualenvs.options.no-pip = false
virtualenvs.options.system-site-packages = false
virtualenvs.path = "{cache-dir}/virtualenvs" # /home/rzuckerm/.cache/pypoetry/virtualenvs
virtualenvs.prompt = "{project_name}-py{python_version}"
virtualenvs.use-poetry-python = false
Python Sysconfig
sysconfig.log
cache-dir = "/home/rzuckerm/.cache/pypoetry"
data-dir = "/home/rzuckerm/.local/share/pypoetry"
installer.max-workers = null
installer.no-binary = null
installer.only-binary = null
installer.parallel = true
installer.re-resolve = true
keyring.enabled = true
python.installation-dir = "{data-dir}/python" # /home/rzuckerm/.local/share/pypoetry/python
requests.max-retries = 0
solver.lazy-wheel = true
system-git-client = false
virtualenvs.create = true
virtualenvs.in-project = null
virtualenvs.options.always-copy = false
virtualenvs.options.no-pip = false
virtualenvs.options.system-site-packages = false
virtualenvs.path = "{cache-dir}/virtualenvs" # /home/rzuckerm/.cache/pypoetry/virtualenvs
virtualenvs.prompt = "{project_name}-py{python_version}"
virtualenvs.use-poetry-python = false
Example pyproject.toml
Before poetry add:
[project]
name = "poetry-example"
version = "0.1.0"
description = ""
authors = [
{name = "rzuckerm"}
]
readme = "README.md"
requires-python = ">=3.10, <4"
dependencies = []
[tool.poetry]
packages = [{include = "poetry_example", from = "src"}]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
After poetry add:
[project]
name = "poetry-example"
version = "0.1.0"
description = ""
authors = [
{name = "rzuckerm"}
]
readme = "README.md"
requires-python = ">=3.10, <4"
dependencies = ["pytest (>=4,<5 || >=6,<7)"]
[tool.poetry]
packages = [{include = "poetry_example", from = "src"}]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
Poetry Runtime Logs
poetry-runtime.log
Stack trace:
1 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/_vendor/lark/lexer.py:665 in lex
663│ while True:
664│ lexer = self.lexers[parser_state.position]
→ 665│ yield lexer.next_token(lexer_state, parser_state)
666│ except EOFError:
667│ pass
UnexpectedCharacters
No terminal matches '|' in the current parser context, at line 1 col 16
pytest (>=4,<5 || >=6,<7)
^
Expected one of:
* _R_PAREN
* _MARKER_SEPARATOR
* _COMMA
Previous tokens: Token('LEGACY_VERSION_CONSTRAINT', '<5')
at ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/_vendor/lark/lexer.py:598 in next_token
594│ if not res:
595│ allowed = self.scanner.allowed_types - self.ignore_types
596│ if not allowed:
597│ allowed = {"<END-OF-FILE>"}
→ 598│ raise UnexpectedCharacters(lex_state.text, line_ctr.char_pos, line_ctr.line, line_ctr.column,
599│ allowed=allowed, token_history=lex_state.last_token and [lex_state.last_token],
600│ state=parser_state, terminals_by_name=self.terminals_by_name)
601│
602│ value, type_ = res
The following error occurred when trying to handle this error:
Stack trace:
8 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/version/requirements.py:43 in __init__
41│
42│ try:
→ 43│ parsed = _parser.parse(requirement_string)
44│ except (UnexpectedCharacters, UnexpectedToken) as e:
45│ raise InvalidRequirementError(
7 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/version/parser.py:31 in parse
29│ )
30│
→ 31│ return self._lark.parse(text=text, **kwargs)
32│
6 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/_vendor/lark/lark.py:655 in parse
653│
654│ """
→ 655│ return self.parser.parse(text, start=start, on_error=on_error)
656│
657│
5 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/_vendor/lark/parser_frontends.py:104 in parse
102│ kw = {} if on_error is None else {'on_error': on_error}
103│ stream = self._make_lexer_thread(text)
→ 104│ return self.parser.parse(stream, chosen_start, **kw)
105│
106│ def parse_interactive(self, text: Optional[str]=None, start=None):
4 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/_vendor/lark/parsers/lalr_parser.py:42 in parse
40│ def parse(self, lexer, start, on_error=None):
41│ try:
→ 42│ return self.parser.parse(lexer, start)
43│ except UnexpectedInput as e:
44│ if on_error is None:
3 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/_vendor/lark/parsers/lalr_parser.py:88 in parse
86│ if start_interactive:
87│ return InteractiveParser(self, parser_state, parser_state.lexer)
→ 88│ return self.parse_from_state(parser_state)
89│
90│
2 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/_vendor/lark/parsers/lalr_parser.py:111 in parse_from_state
109│ except NameError:
110│ pass
→ 111│ raise e
112│ except Exception as e:
113│ if self.debug:
1 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/_vendor/lark/parsers/lalr_parser.py:100 in parse_from_state
98│ try:
99│ token = last_token
→ 100│ for token in state.lexer.lex(state):
101│ assert token is not None
102│ state.feed_token(token)
UnexpectedToken
Unexpected token Token('URI', '||') at line 1, column 16.
Expected one of:
* _R_PAREN
* _COMMA
Previous tokens: [Token('LEGACY_VERSION_CONSTRAINT', '<5')]
at ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/_vendor/lark/lexer.py:674 in lex
670│ # This tests the input against the global context, to provide a nicer error.
671│ try:
672│ last_token = lexer_state.last_token # Save last_token. Calling root_lexer.next_token will change this to the wrong token
673│ token = self.root_lexer.next_token(lexer_state, parser_state)
→ 674│ raise UnexpectedToken(token, e.allowed, state=parser_state, token_history=[last_token], terminals_by_name=self.root_lexer.terminals_by_name)
675│ except UnexpectedCharacters:
676│ raise e # Raise the original UnexpectedCharacters. The root lexer raises it with the wrong expected set.
677│
678│ ###}
The following error occurred when trying to handle this error:
Stack trace:
16 ~/virtualenvs/poetry/lib/python3.12/site-packages/cleo/application.py:327 in run
325│
326│ try:
→ 327│ exit_code = self._run(io)
328│ except BrokenPipeError:
329│ # If we are piped to another process, it may close early and send a
15 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/console/application.py:260 in _run
258│
259│ try:
→ 260│ exit_code = super()._run(io)
261│ except PoetryRuntimeError as e:
262│ io.write_error_line("")
14 ~/virtualenvs/poetry/lib/python3.12/site-packages/cleo/application.py:431 in _run
429│ io.input.interactive(interactive)
430│
→ 431│ exit_code = self._run_command(command, io)
432│ self._running_command = None
433│
13 ~/virtualenvs/poetry/lib/python3.12/site-packages/cleo/application.py:473 in _run_command
471│
472│ if error is not None:
→ 473│ raise error
474│
475│ return terminate_event.exit_code
12 ~/virtualenvs/poetry/lib/python3.12/site-packages/cleo/application.py:454 in _run_command
452│
453│ try:
→ 454│ self._event_dispatcher.dispatch(command_event, COMMAND)
455│
456│ if command_event.command_should_run():
11 ~/virtualenvs/poetry/lib/python3.12/site-packages/cleo/events/event_dispatcher.py:26 in dispatch
24│
25│ if listeners:
→ 26│ self._do_dispatch(listeners, event_name, event)
27│
28│ return event
10 ~/virtualenvs/poetry/lib/python3.12/site-packages/cleo/events/event_dispatcher.py:85 in _do_dispatch
83│ break
84│
→ 85│ listener(event, event_name, self)
86│
87│ def _sort_listeners(self, event_name: str) -> None:
9 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/console/application.py:578 in configure_env
576│
577│ io = event.io
→ 578│ poetry = command.poetry
579│
580│ env_manager = EnvManager(poetry, io=io)
8 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/console/commands/command.py:24 in poetry
22│ def poetry(self) -> Poetry:
23│ if self._poetry is None:
→ 24│ return self.get_application().poetry
25│
26│ return self._poetry
7 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/console/application.py:199 in poetry
197│ return self._poetry
198│
→ 199│ self._poetry = Factory().create_poetry(
200│ cwd=self.project_directory,
201│ io=self._io,
6 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/factory.py:60 in create_poetry
58│ io = NullIO()
59│
→ 60│ base_poetry = super().create_poetry(cwd=cwd, with_groups=with_groups)
61│
62│ if version_str := base_poetry.local_config.get("requires-poetry"):
5 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/factory.py:78 in create_poetry
76│ assert isinstance(version, str)
77│ package = self.get_package(name, version)
→ 78│ self.configure_package(
79│ package, pyproject, poetry_file.parent, with_groups=with_groups
80│ )
4 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/factory.py:164 in configure_package
162│ cls._configure_package_metadata(package, project, tool_poetry, root)
163│ cls._configure_entry_points(package, project, tool_poetry)
→ 164│ cls._configure_package_dependencies(
165│ package=package,
166│ project=project,
3 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/factory.py:396 in _configure_package_dependencies
394│ for constraint in dependencies:
395│ group.add_dependency(
→ 396│ Dependency.create_from_pep_508(
397│ constraint, relative_to=package.root_dir
398│ )
2 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/packages/dependency.py:370 in create_from_pep_508
368│ name += " ;" + rest.split(" ;", 1)[1]
369│
→ 370│ req = parse_requirement(name)
371│
372│ name = req.name
1 ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/version/requirements.py:118 in parse_requirement
116│ @functools.cache
117│ def parse_requirement(requirement_string: str) -> Requirement:
→ 118│ return Requirement(requirement_string)
119│
InvalidRequirementError
The requirement is invalid: Unexpected character at column 16
pytest (>=4,<5 || >=6,<7)
^
at ~/virtualenvs/poetry/lib/python3.12/site-packages/poetry/core/version/requirements.py:45 in __init__
41│
42│ try:
43│ parsed = _parser.parse(requirement_string)
44│ except (UnexpectedCharacters, UnexpectedToken) as e:
→ 45│ raise InvalidRequirementError(
46│ "The requirement is invalid: Unexpected character at column"
47│ f" {e.column}\n\n{e.get_context(requirement_string)}"
48│ )
49│
This is where invalid constraints are created. This is probably not that easy to fix in general.
I understand that this is a weird use-case and that the logical OR operator is not part of PEP 440. However, this did work just fine in poetry 1.x. My guess is that cases like this should actually go in the old tool.poetry.dependencies or tool.poetry.<group>.dependencies instead of project.dependencies.
OK, I just verified that that is the case. If I comment out project.dependencies, I can run `poetry add 'pytest=^4|^6':
[project]
name = "poetry-example"
version = "0.1.0"
description = ""
authors = [
{name = "Ron Zuckerman",email = "[email protected]"}
]
readme = "README.md"
requires-python = ">=3.10, <4"
#dependencies = [
#]
[tool.poetry]
packages = [{include = "poetry_example", from = "src"}]
[tool.poetry.dependencies]
pytest = "^4|^6"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
#7941