Skip to content

Commit 5983929

Browse files
authored
Merge pull request #3 from VCTLabs/el9-compat
el9 compat
2 parents 2f47b72 + 67db58a commit 5983929

File tree

8 files changed

+403
-59
lines changed

8 files changed

+403
-59
lines changed

.git_archival.txt

Lines changed: 0 additions & 3 deletions
This file was deleted.

honcho/backports/__init__.py

Whitespace-only changes.
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
"""
2+
>>> hasattr(entry_points(), 'select')
3+
True
4+
>>> tuple(entry_points(group='console_scripts'))
5+
(...)
6+
7+
Some usage is deprecated and may emit deprecation warnings
8+
on later versions.
9+
10+
>>> import warnings
11+
>>> warnings.filterwarnings('ignore', category=DeprecationWarning)
12+
13+
>>> entry_points()['console_scripts'][0]
14+
EntryPoint...(...)
15+
"""
16+
17+
import collections
18+
import textwrap
19+
import itertools
20+
import operator
21+
import functools
22+
23+
try:
24+
from itertools import filterfalse
25+
except ImportError:
26+
from itertools import ifilterfalse as filterfalse # type: ignore[attr-defined, no-redef]
27+
28+
29+
try:
30+
# prefer importlib_metadata if it has EntryPoints
31+
import importlib_metadata as metadata
32+
33+
if not hasattr(metadata, 'EntryPoints'):
34+
raise ImportError("package without EntryPoints")
35+
from importlib_metadata import distributions, EntryPoint
36+
except ImportError:
37+
try:
38+
import importlib.metadata as metadata # type: ignore[no-redef]
39+
from importlib.metadata import distributions, EntryPoint # type: ignore[assignment]
40+
except ImportError:
41+
from importlib_metadata import (
42+
distributions,
43+
EntryPoint,
44+
)
45+
46+
47+
__all__ = ['entry_points']
48+
49+
50+
def unique_everseen(iterable, key=None):
51+
"List unique elements, preserving order. Remember all elements ever seen."
52+
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
53+
# unique_everseen('ABBCcAD', str.lower) --> A B C D
54+
seen = set()
55+
seen_add = seen.add
56+
if key is None:
57+
for element in filterfalse(seen.__contains__, iterable):
58+
seen_add(element)
59+
yield element
60+
else:
61+
for element in iterable:
62+
k = key(element)
63+
if k not in seen:
64+
seen_add(k)
65+
yield element
66+
67+
68+
class Pair(collections.namedtuple('Pair', 'name value')):
69+
@classmethod
70+
def parse(cls, text):
71+
return cls(*map(str.strip, text.split("=", 1)))
72+
73+
74+
class Sectioned:
75+
"""
76+
A simple entry point config parser for performance
77+
78+
>>> for item in Sectioned.read(Sectioned._sample):
79+
... print(item)
80+
Pair(name='sec1', value='# comments ignored')
81+
Pair(name='sec1', value='a = 1')
82+
Pair(name='sec1', value='b = 2')
83+
Pair(name='sec2', value='a = 2')
84+
85+
>>> res = Sectioned.section_pairs(Sectioned._sample)
86+
>>> item = next(res)
87+
>>> item.name
88+
'sec1'
89+
>>> item.value
90+
Pair(name='a', value='1')
91+
>>> item = next(res)
92+
>>> item.value
93+
Pair(name='b', value='2')
94+
>>> item = next(res)
95+
>>> item.name
96+
'sec2'
97+
>>> item.value
98+
Pair(name='a', value='2')
99+
>>> list(res)
100+
[]
101+
"""
102+
103+
_sample = textwrap.dedent(
104+
"""
105+
[sec1]
106+
# comments ignored
107+
a = 1
108+
b = 2
109+
110+
[sec2]
111+
a = 2
112+
"""
113+
).lstrip()
114+
115+
@classmethod
116+
def section_pairs(cls, text):
117+
return (
118+
section._replace(value=Pair.parse(section.value))
119+
for section in cls.read(text, filter_=cls.valid)
120+
if section.name is not None
121+
)
122+
123+
@staticmethod
124+
def read(text, filter_=None):
125+
lines = filter(filter_, map(str.strip, text.splitlines()))
126+
name = None
127+
for value in lines:
128+
section_match = value.startswith('[') and value.endswith(']')
129+
if section_match:
130+
name = value.strip('[]')
131+
continue
132+
yield Pair(name, value)
133+
134+
@staticmethod
135+
def valid(line):
136+
return line and not line.startswith('#')
137+
138+
139+
def compat_matches(ep, **params):
140+
try:
141+
return ep.matches(**params)
142+
except AttributeError:
143+
pass
144+
attrs = (getattr(ep, param) for param in params)
145+
return all(map(operator.eq, params.values(), attrs))
146+
147+
148+
class EntryPoints(list):
149+
"""
150+
An immutable collection of selectable EntryPoint objects.
151+
"""
152+
153+
__slots__ = ()
154+
155+
def __getitem__(self, name): # -> EntryPoint:
156+
"""
157+
Get the EntryPoint in self matching name.
158+
"""
159+
if isinstance(name, int):
160+
return super(EntryPoints, self).__getitem__(name)
161+
try:
162+
return next(iter(self.select(name=name)))
163+
except StopIteration:
164+
raise KeyError(name)
165+
166+
def select(self, **params):
167+
"""
168+
Select entry points from self that match the
169+
given parameters (typically group and/or name).
170+
"""
171+
return EntryPoints(ep for ep in self if compat_matches(ep, **params))
172+
173+
@property
174+
def names(self):
175+
"""
176+
Return the set of all names of all entry points.
177+
"""
178+
return set(ep.name for ep in self)
179+
180+
@property
181+
def groups(self):
182+
"""
183+
Return the set of all groups of all entry points.
184+
185+
For coverage while SelectableGroups is present.
186+
>>> EntryPoints().groups
187+
set(...)
188+
"""
189+
return set(ep.group for ep in self)
190+
191+
@classmethod
192+
def _from_text_for(cls, text, dist):
193+
return cls(ep._for(dist) for ep in cls._from_text(text))
194+
195+
@classmethod
196+
def _from_text(cls, text):
197+
return itertools.starmap(EntryPoint, cls._parse_groups(text or ''))
198+
199+
@staticmethod
200+
def _parse_groups(text):
201+
return (
202+
(item.value.name, item.value.value, item.name)
203+
for item in Sectioned.section_pairs(text)
204+
)
205+
206+
207+
class SelectableGroups(dict):
208+
"""
209+
A backward- and forward-compatible result from
210+
entry_points that fully implements the dict interface.
211+
"""
212+
213+
@classmethod
214+
def load(cls, eps):
215+
by_group = operator.attrgetter('group')
216+
ordered = sorted(eps, key=by_group)
217+
grouped = itertools.groupby(ordered, by_group)
218+
return cls((group, EntryPoints(eps)) for group, eps in grouped)
219+
220+
@property
221+
def _all(self):
222+
"""
223+
Reconstruct a list of all entrypoints from the groups.
224+
"""
225+
return EntryPoints(itertools.chain.from_iterable(self.values()))
226+
227+
@property
228+
def groups(self):
229+
return self._all.groups
230+
231+
@property
232+
def names(self):
233+
"""
234+
for coverage:
235+
>>> SelectableGroups().names
236+
set(...)
237+
"""
238+
return self._all.names
239+
240+
def select(self, **params):
241+
if not params:
242+
return self
243+
return self._all.select(**params)
244+
245+
246+
def entry_points_compat(**params):
247+
"""Return EntryPoint objects for all installed packages.
248+
249+
Pass selection parameters (group or name) to filter the
250+
result to entry points matching those properties (see
251+
EntryPoints.select()).
252+
253+
For compatibility, returns ``SelectableGroups`` object unless
254+
selection parameters are supplied. In the future, this function
255+
will return ``EntryPoints`` instead of ``SelectableGroups``
256+
even when no selection parameters are supplied.
257+
258+
For maximum future compatibility, pass selection parameters
259+
or invoke ``.select`` with parameters on the result.
260+
261+
:return: EntryPoints or SelectableGroups for all installed packages.
262+
"""
263+
264+
def dist_name(dist):
265+
return dist.metadata['Name']
266+
267+
unique = functools.partial(unique_everseen, key=dist_name)
268+
eps = itertools.chain.from_iterable(
269+
dist.entry_points for dist in unique(distributions())
270+
)
271+
return SelectableGroups.load(eps).select(**params)
272+
273+
274+
needs_backport = not hasattr(metadata, 'EntryPoints') or issubclass(
275+
metadata.EntryPoints, tuple
276+
)
277+
278+
entry_points = entry_points_compat if needs_backport else metadata.entry_points

honcho/command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from honcho.process import Popen
1515

1616
if sys.version_info < (3, 10):
17-
from backports.entry_points_selectable import entry_points
17+
from honcho.backports.entry_points_selectable import entry_points
1818
else:
1919
from importlib.metadata import entry_points
2020

pyproject.toml

Lines changed: 6 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,16 @@
11
[build-system]
2-
requires = ["setuptools", "setuptools_scm[toml]"]
3-
build-backend = "setuptools.build_meta"
4-
5-
[project]
6-
name = "honcho"
7-
description = "Honcho: a Python clone of Foreman. For managing Procfile-based applications."
8-
readme = "README.rst"
9-
authors = [
10-
{name = "Nick Stenning", email = "nick@whiteink.com"}
11-
]
12-
keywords = ["sysadmin", "process", "procfile"]
13-
urls."Source" = "https://github.com/nickstenning/honcho"
14-
classifiers = [
15-
"Environment :: Console",
16-
"Intended Audience :: Developers",
17-
"Intended Audience :: System Administrators",
18-
"License :: OSI Approved :: MIT License",
19-
"Operating System :: OS Independent",
20-
"Programming Language :: Python",
21-
"Programming Language :: Python :: 3",
22-
"Programming Language :: Python :: 3.8",
23-
"Programming Language :: Python :: 3.9",
24-
"Programming Language :: Python :: 3.10",
25-
"Programming Language :: Python :: 3.11",
26-
"Programming Language :: Python :: 3.12",
27-
"Programming Language :: Python :: 3.13",
28-
"Programming Language :: Python :: Implementation :: CPython",
29-
"Programming Language :: Python :: Implementation :: PyPy",
30-
]
31-
dependencies = [
32-
'backports.entry-points-selectable; python_version<"3.10"',
33-
'colorama; sys_platform=="win32"',
2+
requires = [
3+
"setuptools",
4+
"setuptools_scm[toml]",
345
]
35-
dynamic = ["version"]
36-
37-
[project.optional-dependencies]
38-
export = ["jinja2>=3.1.2,<4"]
39-
docs = ["sphinx"]
40-
41-
[project.scripts]
42-
honcho = "honcho.command:main"
6+
build-backend = "setuptools.build_meta"
437

44-
[project.entry-points.honcho_exporters]
45-
runit = "honcho.export.runit:Export"
46-
supervisord = "honcho.export.supervisord:Export"
47-
systemd = "honcho.export.systemd:Export"
48-
upstart = "honcho.export.upstart:Export"
8+
[tool.setuptools_scm]
9+
write_to = "honcho/_version.py"
4910

5011
[tool.ruff]
5112
extend-exclude = [
5213
"doc/conf.py",
5314
"honcho/_version.py",
5415
]
5516
lint.select = ["E4", "E7", "E9", "F", "I", "N", "RUF"]
56-
57-
[tool.setuptools.packages.find]
58-
where = ["."]
59-
include = ["honcho*"]
60-
exclude = ["tests*"]
61-
62-
[tool.setuptools_scm]
63-
write_to = "honcho/_version.py"

0 commit comments

Comments
 (0)