Skip to content

Commit 236cea8

Browse files
authored
Merge pull request #285 from anntzer/epp
Use a hand-written parser for entry points.
2 parents a06aa30 + a0f0ba6 commit 236cea8

File tree

2 files changed

+73
-18
lines changed

2 files changed

+73
-18
lines changed

CHANGES.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ v3.9.0
1414
Preferably, switch to the ``select`` interface introduced
1515
in 3.7.0.
1616

17+
* #283: Entry point parsing no longer relies on ConfigParser
18+
and instead uses a custom, one-pass parser to load the
19+
config, resulting in a ~20% performance improvement when
20+
loading entry points.
21+
1722
v3.8.0
1823
======
1924

importlib_metadata/__init__.py

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import email
88
import pathlib
99
import operator
10+
import textwrap
1011
import warnings
1112
import functools
1213
import itertools
@@ -24,7 +25,6 @@
2425
from ._functools import method_cache
2526
from ._itertools import unique_everseen
2627

27-
from configparser import ConfigParser
2828
from contextlib import suppress
2929
from importlib import import_module
3030
from importlib.abc import MetaPathFinder
@@ -60,6 +60,60 @@ def name(self):
6060
return name
6161

6262

63+
class Sectioned:
64+
"""
65+
A simple entry point config parser for performance
66+
67+
>>> res = Sectioned.get_sections(Sectioned._sample)
68+
>>> sec, values = next(res)
69+
>>> sec
70+
'sec1'
71+
>>> [(key, value) for key, value in values]
72+
[('a', '1'), ('b', '2')]
73+
>>> sec, values = next(res)
74+
>>> sec
75+
'sec2'
76+
>>> [(key, value) for key, value in values]
77+
[('a', '2')]
78+
>>> list(res)
79+
[]
80+
"""
81+
82+
_sample = textwrap.dedent(
83+
"""
84+
[sec1]
85+
a = 1
86+
b = 2
87+
88+
[sec2]
89+
a = 2
90+
"""
91+
).lstrip()
92+
93+
def __init__(self):
94+
self.section = None
95+
96+
def __call__(self, line):
97+
if line.startswith('[') and line.endswith(']'):
98+
# new section
99+
self.section = line.strip('[]')
100+
return
101+
return self.section
102+
103+
@classmethod
104+
def get_sections(cls, text):
105+
lines = filter(None, map(str.strip, text.splitlines()))
106+
return (
107+
(section, map(cls.parse_value, values))
108+
for section, values in itertools.groupby(lines, cls())
109+
if section is not None
110+
)
111+
112+
@staticmethod
113+
def parse_value(line):
114+
return map(str.strip, line.split("=", 1))
115+
116+
63117
class EntryPoint(
64118
PyPy_repr, collections.namedtuple('EntryPointBase', 'name value group')
65119
):
@@ -118,22 +172,6 @@ def extras(self):
118172
match = self.pattern.match(self.value)
119173
return list(re.finditer(r'\w+', match.group('extras') or ''))
120174

121-
@classmethod
122-
def _from_config(cls, config):
123-
return (
124-
cls(name, value, group)
125-
for group in config.sections()
126-
for name, value in config.items(group)
127-
)
128-
129-
@classmethod
130-
def _from_text(cls, text):
131-
config = ConfigParser(delimiters='=')
132-
# case sensitive: https://stackoverflow.com/q/1611799/812183
133-
config.optionxform = str
134-
config.read_string(text)
135-
return cls._from_config(config)
136-
137175
def _for(self, dist):
138176
self.dist = dist
139177
return self
@@ -203,7 +241,19 @@ def groups(self):
203241

204242
@classmethod
205243
def _from_text_for(cls, text, dist):
206-
return cls(ep._for(dist) for ep in EntryPoint._from_text(text))
244+
return cls(ep._for(dist) for ep in cls._from_text(text))
245+
246+
@classmethod
247+
def _from_text(cls, text):
248+
return itertools.starmap(EntryPoint, cls._parse_groups(text or ''))
249+
250+
@staticmethod
251+
def _parse_groups(text):
252+
return (
253+
(name, value, section)
254+
for section, values in Sectioned.get_sections(text)
255+
for name, value in values
256+
)
207257

208258

209259
def flake8_bypass(func):

0 commit comments

Comments
 (0)