352 lines
11 KiB
Python
352 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
internal gevent utilities, not for external use.
|
|
"""
|
|
|
|
# Be very careful not to import anything that would cause issues with
|
|
# monkey-patching.
|
|
|
|
from __future__ import print_function, absolute_import, division
|
|
|
|
from gevent._compat import iteritems
|
|
|
|
|
|
class _NONE(object):
|
|
"""
|
|
A special object you must never pass to any gevent API.
|
|
Used as a marker object for keyword arguments that cannot have the
|
|
builtin None (because that might be a valid value).
|
|
"""
|
|
__slots__ = ()
|
|
|
|
def __repr__(self):
|
|
return '<default value>'
|
|
|
|
_NONE = _NONE()
|
|
|
|
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
|
|
'__annotations__')
|
|
WRAPPER_UPDATES = ('__dict__',)
|
|
def update_wrapper(wrapper,
|
|
wrapped,
|
|
assigned=WRAPPER_ASSIGNMENTS,
|
|
updated=WRAPPER_UPDATES):
|
|
"""
|
|
Based on code from the standard library ``functools``, but
|
|
doesn't perform any of the troublesome imports.
|
|
|
|
functools imports RLock from _thread for purposes of the
|
|
``lru_cache``, making it problematic to use from gevent.
|
|
|
|
The other imports are somewhat heavy: abc, collections, types.
|
|
"""
|
|
for attr in assigned:
|
|
try:
|
|
value = getattr(wrapped, attr)
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
setattr(wrapper, attr, value)
|
|
for attr in updated:
|
|
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
|
|
# Issue #17482: set __wrapped__ last so we don't inadvertently copy it
|
|
# from the wrapped function when updating __dict__
|
|
wrapper.__wrapped__ = wrapped
|
|
# Return the wrapper so this can be used as a decorator via partial()
|
|
return wrapper
|
|
|
|
|
|
def copy_globals(source,
|
|
globs,
|
|
only_names=None,
|
|
ignore_missing_names=False,
|
|
names_to_ignore=(),
|
|
dunder_names_to_keep=('__implements__', '__all__', '__imports__'),
|
|
cleanup_globs=True):
|
|
"""
|
|
Copy attributes defined in ``source.__dict__`` to the dictionary
|
|
in globs (which should be the caller's :func:`globals`).
|
|
|
|
Names that start with ``__`` are ignored (unless they are in
|
|
*dunder_names_to_keep*). Anything found in *names_to_ignore* is
|
|
also ignored.
|
|
|
|
If *only_names* is given, only those attributes will be
|
|
considered. In this case, *ignore_missing_names* says whether or
|
|
not to raise an :exc:`AttributeError` if one of those names can't
|
|
be found.
|
|
|
|
If *cleanup_globs* has a true value, then common things imported but
|
|
not used at runtime are removed, including this function.
|
|
|
|
Returns a list of the names copied; this should be assigned to ``__imports__``.
|
|
"""
|
|
if only_names:
|
|
if ignore_missing_names:
|
|
items = ((k, getattr(source, k, _NONE)) for k in only_names)
|
|
else:
|
|
items = ((k, getattr(source, k)) for k in only_names)
|
|
else:
|
|
items = iteritems(source.__dict__)
|
|
|
|
copied = []
|
|
for key, value in items:
|
|
if value is _NONE:
|
|
continue
|
|
if key in names_to_ignore:
|
|
continue
|
|
if key.startswith("__") and key not in dunder_names_to_keep:
|
|
continue
|
|
globs[key] = value
|
|
copied.append(key)
|
|
|
|
if cleanup_globs:
|
|
if 'copy_globals' in globs:
|
|
del globs['copy_globals']
|
|
|
|
return copied
|
|
|
|
def import_c_accel(globs, cname):
|
|
"""
|
|
Import the C-accelerator for the *cname*
|
|
and copy its globals.
|
|
|
|
The *cname* should be hardcoded to match the expected
|
|
C accelerator module.
|
|
|
|
Unless PURE_PYTHON is set (in the environment or automatically
|
|
on PyPy), then the C-accelerator is required.
|
|
"""
|
|
if not cname.startswith('gevent._gevent_c'):
|
|
# Old module code that hasn't been updated yet.
|
|
cname = cname.replace('gevent._',
|
|
'gevent._gevent_c')
|
|
|
|
name = globs.get('__name__')
|
|
|
|
if not name or name == cname:
|
|
# Do nothing if we're being exec'd as a file (no name)
|
|
# or we're running from the C extension
|
|
return
|
|
|
|
|
|
from gevent._compat import PURE_PYTHON
|
|
if PURE_PYTHON:
|
|
return
|
|
|
|
import importlib
|
|
import warnings
|
|
with warnings.catch_warnings():
|
|
# Python 3.7 likes to produce
|
|
# "ImportWarning: can't resolve
|
|
# package from __spec__ or __package__, falling back on
|
|
# __name__ and __path__"
|
|
# when we load cython compiled files. This is probably a bug in
|
|
# Cython, but it doesn't seem to have any consequences, it's
|
|
# just annoying to see and can mess up our unittests.
|
|
warnings.simplefilter('ignore', ImportWarning)
|
|
mod = importlib.import_module(cname)
|
|
|
|
# By adopting the entire __dict__, we get a more accurate
|
|
# __file__ and module repr, plus we don't leak any imported
|
|
# things we no longer need.
|
|
globs.clear()
|
|
globs.update(mod.__dict__)
|
|
|
|
if 'import_c_accel' in globs:
|
|
del globs['import_c_accel']
|
|
|
|
|
|
class Lazy(object):
|
|
"""
|
|
A non-data descriptor used just like @property. The
|
|
difference is the function value is assigned to the instance
|
|
dict the first time it is accessed and then the function is never
|
|
called again.
|
|
|
|
Contrast with `readproperty`.
|
|
"""
|
|
def __init__(self, func):
|
|
self.data = (func, func.__name__)
|
|
update_wrapper(self, func)
|
|
|
|
def __get__(self, inst, class_):
|
|
if inst is None:
|
|
return self
|
|
|
|
func, name = self.data
|
|
value = func(inst)
|
|
inst.__dict__[name] = value
|
|
return value
|
|
|
|
class readproperty(object):
|
|
"""
|
|
A non-data descriptor similar to :class:`property`.
|
|
|
|
The difference is that the property can be assigned to directly,
|
|
without invoking a setter function. When the property is assigned
|
|
to, it is cached in the instance and the function is not called on
|
|
that instance again.
|
|
|
|
Contrast with `Lazy`, which caches the result of the function in the
|
|
instance the first time it is called and never calls the function on that
|
|
instance again.
|
|
"""
|
|
|
|
def __init__(self, func):
|
|
self.func = func
|
|
update_wrapper(self, func)
|
|
|
|
def __get__(self, inst, class_):
|
|
if inst is None:
|
|
return self
|
|
|
|
return self.func(inst)
|
|
|
|
class LazyOnClass(object):
|
|
"""
|
|
Similar to `Lazy`, but stores the value in the class.
|
|
|
|
This is useful when the getter is expensive and conceptually
|
|
a shared class value, but we don't want import-time side-effects
|
|
such as expensive imports because it may not always be used.
|
|
|
|
Probably doesn't mix well with inheritance?
|
|
"""
|
|
|
|
@classmethod
|
|
def lazy(cls, cls_dict, func):
|
|
"Put a LazyOnClass object in *cls_dict* with the same name as *func*"
|
|
cls_dict[func.__name__] = cls(func)
|
|
|
|
def __init__(self, func, name=None):
|
|
self.name = name or func.__name__
|
|
self.func = func
|
|
|
|
def __get__(self, inst, klass):
|
|
if inst is None: # pragma: no cover
|
|
return self
|
|
|
|
val = self.func(inst)
|
|
setattr(klass, self.name, val)
|
|
return val
|
|
|
|
|
|
def gmctime():
|
|
"""
|
|
Returns the current time as a string in RFC3339 format.
|
|
"""
|
|
import time
|
|
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
|
|
|
|
###
|
|
# Release automation.
|
|
#
|
|
# Most of this is to integrate zest.releaser with towncrier. There is
|
|
# a plugin package that can do the same:
|
|
# https://github.com/collective/zestreleaser.towncrier
|
|
###
|
|
|
|
def prereleaser_middle(data): # pragma: no cover
|
|
"""
|
|
zest.releaser prerelease middle hook for gevent.
|
|
|
|
The prerelease step:
|
|
|
|
asks you for a version number
|
|
updates the setup.py or version.txt and the
|
|
CHANGES/HISTORY/CHANGELOG file (with either
|
|
this new version
|
|
number and offers to commit those changes to git
|
|
|
|
The middle hook:
|
|
|
|
All data dictionary items are available and some questions
|
|
(like new version number) have been asked.
|
|
No filesystem changes have been made yet.
|
|
|
|
It is our job to finish up the filesystem changes needed, including:
|
|
|
|
- Calling towncrier to handle CHANGES.rst
|
|
- Add the version number to ``versionadded``, ``versionchanged`` and
|
|
``deprecated`` directives in Python source.
|
|
"""
|
|
if data['name'] != 'gevent':
|
|
# We are specified in ``setup.cfg``, not ``setup.py``, so we do not
|
|
# come into play for other projects, only this one. We shouldn't
|
|
# need this check, but there it is.
|
|
return
|
|
|
|
import re
|
|
import os
|
|
import subprocess
|
|
from gevent.testing import modules
|
|
|
|
new_version = data['new_version']
|
|
|
|
# Generate CHANGES.rst, remove old news entries.
|
|
subprocess.check_call([
|
|
'towncrier',
|
|
'build',
|
|
'--version', data['new_version'],
|
|
'--yes'
|
|
])
|
|
|
|
data['update_history'] = False # Because towncrier already did.
|
|
|
|
# But unstage it; we want it to show in the diff zest.releaser will do
|
|
subprocess.check_call([
|
|
'git',
|
|
'restore',
|
|
'--staged',
|
|
'CHANGES.rst',
|
|
])
|
|
|
|
# Put the version number in source files.
|
|
regex = re.compile(b'.. (versionchanged|versionadded|deprecated):: NEXT')
|
|
if not isinstance(new_version, bytes):
|
|
new_version_bytes = new_version.encode('ascii')
|
|
else:
|
|
new_version_bytes = new_version
|
|
new_version_bytes = new_version.encode('ascii')
|
|
replacement = br'.. \1:: %s' % (new_version_bytes,)
|
|
# TODO: This should also look in the docs/ directory at
|
|
# *.rst
|
|
for path, _ in modules.walk_modules(
|
|
# Start here
|
|
basedir=os.path.join(data['reporoot'], 'src', 'gevent'),
|
|
# Include sub-dirs
|
|
recursive=True,
|
|
# Include tests
|
|
include_tests=True,
|
|
# and other things usually excluded
|
|
excluded_modules=(),
|
|
# Don't return build binaries
|
|
include_so=False,
|
|
# Don't try to import things; we want all files.
|
|
check_optional=False,
|
|
):
|
|
with open(path, 'rb') as f:
|
|
contents = f.read()
|
|
new_contents, count = regex.subn(replacement, contents)
|
|
if count:
|
|
print("Replaced version NEXT in", path)
|
|
with open(path, 'wb') as f:
|
|
f.write(new_contents)
|
|
|
|
def postreleaser_before(data): # pragma: no cover
|
|
"""
|
|
Prevents zest.releaser from modifying the CHANGES.rst to add the
|
|
'no changes yet' section; towncrier is in charge of CHANGES.rst.
|
|
|
|
Needs zest.releaser 6.15.0.
|
|
"""
|
|
if data['name'] != 'gevent':
|
|
# We are specified in ``setup.cfg``, not ``setup.py``, so we do not
|
|
# come into play for other projects, only this one. We shouldn't
|
|
# need this check, but there it is.
|
|
return
|
|
|
|
data['update_history'] = False
|