poetry icon indicating copy to clipboard operation
poetry copied to clipboard

`poetry add` with `||` operator breaks further poetry commands

Open rzuckerm opened this issue 3 months ago • 4 comments

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│

rzuckerm avatar Sep 24 '25 16:09 rzuckerm

This is where invalid constraints are created. This is probably not that easy to fix in general.

radoering avatar Sep 26 '25 15:09 radoering

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.

rzuckerm avatar Sep 26 '25 18:09 rzuckerm

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"

rzuckerm avatar Sep 26 '25 18:09 rzuckerm

#7941

dimbleby avatar Sep 26 '25 22:09 dimbleby