From c983aa4edc01bd215ead05edd6dd080e859af24e Mon Sep 17 00:00:00 2001 From: Christian Schmidbauer Date: Tue, 30 Sep 2025 16:02:45 +0200 Subject: [PATCH] feat: accumulate duplicate requirements --- morgan/__init__.py | 6 ++++-- morgan/utils.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/morgan/__init__.py b/morgan/__init__.py index 755fed9..7fe2227 100644 --- a/morgan/__init__.py +++ b/morgan/__init__.py @@ -20,7 +20,7 @@ from morgan import configurator, metadata, server from morgan.__about__ import __version__ -from morgan.utils import Cache, to_single_dash +from morgan.utils import Cache, ListExtendingOrderedDict, to_single_dash PYPI_ADDRESS = "https://pypi.org/simple/" PREFERRED_HASH_ALG = "sha256" @@ -44,7 +44,9 @@ def __init__(self, args: argparse.Namespace): self.index_path = args.index_path self.index_url = args.index_url self.mirror_all_versions: bool = args.mirror_all_versions - self.config = configparser.ConfigParser() + self.config = configparser.ConfigParser( + strict=False, dict_type=ListExtendingOrderedDict + ) self.config.read(args.config) self.envs = {} self._supported_pyversions = [] diff --git a/morgan/utils.py b/morgan/utils.py index 425efc1..1acae51 100644 --- a/morgan/utils.py +++ b/morgan/utils.py @@ -1,4 +1,5 @@ import re +from collections import OrderedDict from packaging.requirements import Requirement @@ -42,3 +43,39 @@ def is_simple_case(self, req): if all(spec.operator in ('>', '>=') for spec in specifier._specs): return True return False + + +class ListExtendingOrderedDict(OrderedDict): + """An OrderedDict subclass that aggregates list values for duplicate keys. + + This class extends OrderedDict to provide special handling for list values. + When a list value is assigned to an existing key, the new list is extended + onto the existing list instead of replacing it. + + In the context of configparser, this allows for accumulating multiple values + from different sections or repeated keys, such as in multiline requirements. + + Examples: + >>> d = MultiOrderedDict() + >>> d['key'] = [1, 2] + >>> d['key'] = [3, 4] + >>> d['key'] + [1, 2, 3, 4] + >>> d['other'] = 'value' + >>> d['other'] = 'new_value' # Non-list values behave normally + >>> d['other'] + 'new_value' + """ + + def __setitem__(self, key, value): + """Sets the value for the given key, extending lists if the key exists. + + Args: + key: The dictionary key. + value: The value to set. If this is a list and the key already exists, + the list will be extended to the existing value instead of replacing it. + """ + if isinstance(value, list) and key in self: + self[key].extend(value) + else: + super().__setitem__(key, value)