Commit 12766d18 authored by Kirill Smelkov's avatar Kirill Smelkov

Merge branch 'master' into y/bstr+x/gpystr

* master:
  golang: Add support for @func(Class) and @func to be used over @property
  golang: Make @func to be idempotent
  golang: Adjust @func to wrap functions with standalone wrapper with recognizable name
  gpython: Implement -v
  gpython: Implement -X for non-gpython options
  gpython: Implement -E
  golang: test: Fix for Pytest < 7
parents 2cce2770 91a434d5
Pipeline #38565 running with stage
in 0 seconds
...@@ -74,10 +74,25 @@ def _meth(cls, fcall): ...@@ -74,10 +74,25 @@ def _meth(cls, fcall):
# wrap f with @_func, so that e.g. defer works automatically. # wrap f with @_func, so that e.g. defer works automatically.
f = _func(f) f = _func(f)
if isinstance(f, (staticmethod, classmethod)): # property is special - it has up to 3 functions inside
func_name = f.__func__.__name__ # but the only practical case is when it starts with
#
# @func(Class)
# @property
# def ...
#
# which means that .fget should be set and so we use get name as the
# name of the method.
f_ = f
if isinstance(f, property):
f_ = f.fget
if f_ is None:
raise ValueError("func(cls) used on property without getter")
if isinstance(f_, (staticmethod, classmethod)):
func_name = f_.__func__.__name__
else: else:
func_name = f.__name__ func_name = f_.__name__
setattr(cls, func_name, f) setattr(cls, func_name, f)
# if `@func(cls) def name` caller already has `name` set, don't override it # if `@func(cls) def name` caller already has `name` set, don't override it
...@@ -111,21 +126,32 @@ class _DelAttrAfterMeth(object): ...@@ -111,21 +126,32 @@ class _DelAttrAfterMeth(object):
# _func serves @func. # _func serves @func.
def _func(f): def _func(f):
# @staticmethod & friends require special care: # @property is special: there are 3 functions inside and we need to wrap
# them all with repacking back into property.
if isinstance(f, property):
fget = fset = fdel = None
if f.fget is not None:
fget = _func(f.fget)
if f.fset is not None:
fset = _func(f.fset)
if f.fdel is not None:
fdel = _func(f.fdel)
return type(f)(fget, fset, fdel, f.__doc__)
# @staticmethod & friends also require special care:
# unpack f first to original func and then repack back after wrapping. # unpack f first to original func and then repack back after wrapping.
fclass = None fclass = None
if isinstance(f, (staticmethod, classmethod)): if isinstance(f, (staticmethod, classmethod)):
fclass = type(f) fclass = type(f)
f = f.__func__ f = f.__func__
def _(f, *argv, **kw): # prepare function that runs f under separate frame, where defer will register calls
# run f under separate frame, where defer will register calls. # keep all f attributes, like __name__, __doc__, etc on the wrapper
__goframe__ = _GoFrame() # if f was already wrapped with _func - no need to wrap it again
with __goframe__: _ = f
return f(*argv, **kw) if getattr(f, '__go_wrapper__', None) is not _goframe:
_ = decorator.decorate(f, _goframe)
# keep all f attributes, like __name__, __doc__, etc on _ _.__go_wrapper__ = _goframe
_ = decorator.decorate(f, _)
# repack _ into e.g. @staticmethod if that was used on f. # repack _ into e.g. @staticmethod if that was used on f.
if fclass is not None: if fclass is not None:
...@@ -133,7 +159,13 @@ def _func(f): ...@@ -133,7 +159,13 @@ def _func(f):
return _ return _
# _GoFrame serves __goframe__ that is setup by @func. # _goframe is used by @func to run f under separate frame.
def _goframe(f, *argv, **kw):
__goframe__ = _GoFrame()
with __goframe__:
return f(*argv, **kw)
# _GoFrame serves __goframe__ that is setup by _goframe.
class _GoFrame: class _GoFrame:
def __init__(self): def __init__(self):
self.deferv = [] # defer registers funcs here self.deferv = [] # defer registers funcs here
......
...@@ -973,7 +973,7 @@ def test_func(): ...@@ -973,7 +973,7 @@ def test_func():
# test how @func(cls) works # test how @func(cls) works
# this also implicitly tests just @func, since @func(cls) uses that. # this also implicitly tests just @func, since @func(cls) uses that.
class MyClass: class MyClass(object):
def __init__(self, v): def __init__(self, v):
self.v = v self.v = v
...@@ -1013,11 +1013,48 @@ def test_func(): ...@@ -1013,11 +1013,48 @@ def test_func():
gc.collect() # pypy needs this to trigger _DelAttrAfterMeth GC gc.collect() # pypy needs this to trigger _DelAttrAfterMeth GC
assert 'var' not in locals() assert 'var' not in locals()
vproperty = mproperty_orig = 'vproperty'
@func(MyClass)
@property
def vproperty(self):
"""documentation for vproperty"""
assert isinstance(self, MyClass)
return 'v%s' % self.v
assert vproperty is mproperty_orig
assert vproperty == 'vproperty'
@func(MyClass)
@MyClass.vproperty.setter
def _(self, v):
assert isinstance(self, MyClass)
self.v = v
assert vproperty is mproperty_orig
assert vproperty == 'vproperty'
@func(MyClass)
@MyClass.vproperty.deleter
def _(self):
assert isinstance(self, MyClass)
self.v = 'deleted'
assert vproperty is mproperty_orig
assert vproperty == 'vproperty'
obj = MyClass(4) obj = MyClass(4)
assert obj.zzz(4) == 4 + 1 assert obj.zzz(4) == 4 + 1
assert obj.mstatic(5) == 5 + 1 assert obj.mstatic(5) == 5 + 1
assert obj.mcls(7) == 7 + 1 assert obj.mcls(7) == 7 + 1
assert obj.var(8) == 8 + 1 assert obj.var(8) == 8 + 1
assert obj.v == 4 # set by .zzz
assert obj.vproperty == 'v4'
obj.vproperty = 5
assert obj.v == 5
assert obj.vproperty == 'v5'
del obj.vproperty
assert obj.v == 'deleted'
assert obj.vproperty == 'vdeleted'
assert MyClass.vproperty.__doc__ == "documentation for vproperty"""
# this tests that @func (used by @func(cls)) preserves decorated function signature # this tests that @func (used by @func(cls)) preserves decorated function signature
assert fmtargspec(MyClass.zzz) == '(self, v, x=2, **kkkkwww)' assert fmtargspec(MyClass.zzz) == '(self, v, x=2, **kkkkwww)'
...@@ -1034,6 +1071,21 @@ def test_func(): ...@@ -1034,6 +1071,21 @@ def test_func():
assert MyClass.var.__module__ == __name__ assert MyClass.var.__module__ == __name__
assert MyClass.var.__name__ == 'var' assert MyClass.var.__name__ == 'var'
assert MyClass.vproperty.fget.__module__ == __name__
assert MyClass.vproperty.fset.__module__ == __name__
assert MyClass.vproperty.fdel.__module__ == __name__
assert MyClass.vproperty.fget.__name__ == 'vproperty'
assert MyClass.vproperty.fset.__name__ == '_'
assert MyClass.vproperty.fdel.__name__ == '_'
# test that func·func = func (double _func calls are done internally for
# getter when handling @func(@MyClass.vproperty.setter)
def f(): pass
g = func(f)
h = func(g)
assert h is g
# @func overhead at def time. # @func overhead at def time.
def bench_def(b): def bench_def(b):
for i in xrange(b.N): for i in xrange(b.N):
...@@ -1257,6 +1309,45 @@ def test_deferrecover(): ...@@ -1257,6 +1309,45 @@ def test_deferrecover():
assert v == [7, 2, 1] assert v == [7, 2, 1]
# defer in std @property
v = []
class MyClass(object):
@func
@property
def vproperty(self):
"""vproperty doc"""
defer(lambda: v.append(1))
defer(lambda: v.append(3))
defer(lambda: v.append(4))
return 'v'
@func
@vproperty.setter
def vproperty(self, val):
defer(lambda: v.append(1))
defer(lambda: v.append(4))
defer(lambda: v.append(val))
@func
@vproperty.deleter
def vproperty(self):
defer(lambda: v.append(1))
defer(lambda: v.append(5))
defer(lambda: v.append('del'))
obj = MyClass()
assert MyClass.vproperty.__doc__ == "vproperty doc"
assert obj.vproperty == 'v'
assert v == [4, 3, 1]
v = []
obj.vproperty = 'q'
assert v == ['q', 4, 1]
v = []
del obj.vproperty
assert v == ['del', 5, 1]
# verify that defer correctly establishes exception chain (even on py2). # verify that defer correctly establishes exception chain (even on py2).
def test_defer_excchain(): def test_defer_excchain():
# just @func/raise embeds traceback and adds ø chain # just @func/raise embeds traceback and adds ø chain
...@@ -1568,7 +1659,7 @@ RuntimeError: gamma ...@@ -1568,7 +1659,7 @@ RuntimeError: gamma
assertDoc("""\ assertDoc("""\
Traceback (most recent call last): Traceback (most recent call last):
File "PYGOLANG/golang/__init__.py", line ..., in _ File "PYGOLANG/golang/__init__.py", line ..., in _goframe
return f(*argv, **kw) return f(*argv, **kw)
^^^^^^^^^^^^^^ +PY311 ^^^^^^^^^^^^^^ +PY311
File "PYGOLANG/golang/golang_test.py", line ..., in caller File "PYGOLANG/golang/golang_test.py", line ..., in caller
...@@ -1590,7 +1681,7 @@ Traceback (most recent call last): ...@@ -1590,7 +1681,7 @@ Traceback (most recent call last):
File "PYGOLANG/golang/golang_test.py", line ..., in test_defer_excchain_traceback File "PYGOLANG/golang/golang_test.py", line ..., in test_defer_excchain_traceback
caller() caller()
... ...
File "PYGOLANG/golang/__init__.py", line ..., in _ File "PYGOLANG/golang/__init__.py", line ..., in _goframe
return f(*argv, **kw) -PY310 return f(*argv, **kw) -PY310
with __goframe__: +PY310 with __goframe__: +PY310
File "PYGOLANG/golang/__init__.py", line ..., in __exit__ File "PYGOLANG/golang/__init__.py", line ..., in __exit__
...@@ -1609,7 +1700,7 @@ Traceback (most recent call last): ...@@ -1609,7 +1700,7 @@ Traceback (most recent call last):
File "PYGOLANG/golang/golang_test.py", line ..., in test_defer_excchain_traceback File "PYGOLANG/golang/golang_test.py", line ..., in test_defer_excchain_traceback
caller() caller()
... ...
File "PYGOLANG/golang/__init__.py", line ..., in _ File "PYGOLANG/golang/__init__.py", line ..., in _goframe
return f(*argv, **kw) -PY310 return f(*argv, **kw) -PY310
with __goframe__: +PY310 with __goframe__: +PY310
File "PYGOLANG/golang/__init__.py", line ..., in __exit__ File "PYGOLANG/golang/__init__.py", line ..., in __exit__
...@@ -1625,7 +1716,7 @@ RuntimeError: aaa ...@@ -1625,7 +1716,7 @@ RuntimeError: aaa
e.__cause__ = e.__context__ e.__cause__ = e.__context__
assertDoc("""\ assertDoc("""\
Traceback (most recent call last): Traceback (most recent call last):
File "PYGOLANG/golang/__init__.py", line ..., in _ File "PYGOLANG/golang/__init__.py", line ..., in _goframe
return f(*argv, **kw) return f(*argv, **kw)
^^^^^^^^^^^^^^ +PY311 ^^^^^^^^^^^^^^ +PY311
File "PYGOLANG/golang/golang_test.py", line ..., in caller File "PYGOLANG/golang/golang_test.py", line ..., in caller
...@@ -1647,7 +1738,7 @@ Traceback (most recent call last): ...@@ -1647,7 +1738,7 @@ Traceback (most recent call last):
File "PYGOLANG/golang/golang_test.py", line ..., in test_defer_excchain_traceback File "PYGOLANG/golang/golang_test.py", line ..., in test_defer_excchain_traceback
caller() caller()
... ...
File "PYGOLANG/golang/__init__.py", line ..., in _ File "PYGOLANG/golang/__init__.py", line ..., in _goframe
return f(*argv, **kw) -PY310 return f(*argv, **kw) -PY310
with __goframe__: +PY310 with __goframe__: +PY310
File "PYGOLANG/golang/__init__.py", line ..., in __exit__ File "PYGOLANG/golang/__init__.py", line ..., in __exit__
...@@ -1697,7 +1788,7 @@ def test_defer_excchain_dump_pytest(): ...@@ -1697,7 +1788,7 @@ def test_defer_excchain_dump_pytest():
# similarly to ipython we do not need to test it becase we activate # similarly to ipython we do not need to test it becase we activate
# pytest-related patch only on py2 for which latest pytest version is 4.6.11 . # pytest-related patch only on py2 for which latest pytest version is 4.6.11 .
import pytest import pytest
if six.PY3 and pytest.version_tuple >= (7,4): if six.PY3 and getattr(pytest, 'version_tuple', (0,)) >= (7,4):
skip("pytest is patched only on py2; pytest7.4 changed traceback format") skip("pytest is patched only on py2; pytest7.4 changed traceback format")
tbok = readfile(dir_testprog + "/golang_test_defer_excchain.txt-pytest") tbok = readfile(dir_testprog + "/golang_test_defer_excchain.txt-pytest")
retcode, stdout, stderr = _pyrun([ retcode, stdout, stderr = _pyrun([
......
Traceback (most recent call last): Traceback (most recent call last):
File "PYGOLANG/golang/__init__.py", line ..., in _ File "PYGOLANG/golang/__init__.py", line ..., in _goframe
return f(*argv, **kw) return f(*argv, **kw)
^^^^^^^^^^^^^^ +PY311 ^^^^^^^^^^^^^^ +PY311
File "PY39(PYGOLANG/golang/testprog/)golang_test_defer_excchain.py", line 42, in main File "PY39(PYGOLANG/golang/testprog/)golang_test_defer_excchain.py", line 42, in main
...@@ -31,7 +31,7 @@ Traceback (most recent call last): ...@@ -31,7 +31,7 @@ Traceback (most recent call last):
... "PY39(PYGOLANG/golang/testprog/)golang_test_defer_excchain.py", line 45, in <module> ... "PY39(PYGOLANG/golang/testprog/)golang_test_defer_excchain.py", line 45, in <module>
main() main()
... ...
File "PYGOLANG/golang/__init__.py", line ..., in _ File "PYGOLANG/golang/__init__.py", line ..., in _goframe
return f(*argv, **kw) -PY310 return f(*argv, **kw) -PY310
with __goframe__: +PY310 with __goframe__: +PY310
File "PYGOLANG/golang/__init__.py", line ..., in __exit__ File "PYGOLANG/golang/__init__.py", line ..., in __exit__
......
... ...
RuntimeError Traceback (most recent call last) RuntimeError Traceback (most recent call last)
PYGOLANG/golang/__init__.py in _(f, *argv, **kw) PYGOLANG/golang/__init__.py in _goframe(f, *argv, **kw)
... ...
--> ... return f(*argv, **kw) --> ... return f(*argv, **kw)
... ...
...@@ -50,7 +50,7 @@ PYGOLANG/golang/testprog/golang_test_defer_excchain.py in ... ...@@ -50,7 +50,7 @@ PYGOLANG/golang/testprog/golang_test_defer_excchain.py in ...
... ...
PYGOLANG/golang/__init__.py in _(f, *argv, **kw) PYGOLANG/golang/__init__.py in _goframe(f, *argv, **kw)
... ...
--> ... return f(*argv, **kw) --> ... return f(*argv, **kw)
... ...
......
... ...
...________________ main ________________... ...________________ main ________________...
../__init__.py:...: in _ ../__init__.py:...: in _goframe
return f(*argv, **kw) return f(*argv, **kw)
golang_test_defer_excchain.py:42: in main golang_test_defer_excchain.py:42: in main
raise RuntimeError("err") raise RuntimeError("err")
......
...@@ -45,7 +45,7 @@ $GPYTHON_STRINGS=pystd. ...@@ -45,7 +45,7 @@ $GPYTHON_STRINGS=pystd.
from __future__ import print_function, absolute_import from __future__ import print_function, absolute_import
_pyopt = "c:im:OVW:X:" _pyopt = "c:Eim:OvVW:X:"
_pyopt_long = ('version',) _pyopt_long = ('version',)
# pymain mimics `python ...` # pymain mimics `python ...`
...@@ -117,8 +117,21 @@ def pymain(argv, init=None): ...@@ -117,8 +117,21 @@ def pymain(argv, init=None):
for (opt, arg) in igetopt: for (opt, arg) in igetopt:
# options that require reexecuting through underlying python with that -<opt> # options that require reexecuting through underlying python with that -<opt>
if opt in ( if opt in (
'-E', # ignore $PYTHON*
'-O', # optimize '-O', # optimize
'-v', # trace import statements
'-X', # set implementation-specific option
): ):
# but keep `-X gpython.*` in user part of argv in case of reexec
# leaving it for main to handle. If it is only pymain to run, then
# we will be ignoring `-X gpython.*` which goes in line with builtin
# py3 behaviour to ignore any unknown -X option.
if opt == '-X' and arg is not None and arg.startswith('gpython.'):
reexec_argv.append(opt)
reexec_argv.append(arg)
else:
reexec_with.append(opt) reexec_with.append(opt)
if arg is not None: if arg is not None:
reexec_with.append(arg) reexec_with.append(arg)
...@@ -397,13 +410,15 @@ def main(): ...@@ -397,13 +410,15 @@ def main():
# -X gpython.runtime=(gevent|threads) + $GPYTHON_RUNTIME # -X gpython.runtime=(gevent|threads) + $GPYTHON_RUNTIME
# -X gpython.strings=(bstr+ustr|pystd) + $GPYTHON_STRINGS # -X gpython.strings=(bstr+ustr|pystd) + $GPYTHON_STRINGS
sys._xoptions = getattr(sys, '_xoptions', {}) sys._xoptions = getattr(sys, '_xoptions', {})
argv_ = []
gpy_runtime = os.getenv('GPYTHON_RUNTIME', 'gevent') gpy_runtime = os.getenv('GPYTHON_RUNTIME', 'gevent')
gpy_strings = os.getenv('GPYTHON_STRINGS', 'bstr+ustr') gpy_strings = os.getenv('GPYTHON_STRINGS', 'bstr+ustr')
igetopt = _IGetOpt(sys.argv[1:], _pyopt, _pyopt_long) igetopt = _IGetOpt(sys.argv[1:], _pyopt, _pyopt_long)
for (opt, arg) in igetopt: for (opt, arg) in igetopt:
if opt == '-X': if opt == '-X':
if arg.startswith('gpython.'): # any non gpython -X option is handled by pymain; ignore them here
if not arg.startswith('gpython.'):
continue
if arg.startswith('gpython.runtime='): if arg.startswith('gpython.runtime='):
gpy_runtime = arg[len('gpython.runtime='):] gpy_runtime = arg[len('gpython.runtime='):]
sys._xoptions['gpython.runtime'] = gpy_runtime sys._xoptions['gpython.runtime'] = gpy_runtime
...@@ -417,15 +432,10 @@ def main(): ...@@ -417,15 +432,10 @@ def main():
continue continue
argv_.append(opt)
if arg is not None:
argv_.append(arg)
# options after -c / -m are not for python itself # options after -c / -m are not for python itself
if opt in ('-c', '-m'): if opt in ('-c', '-m'):
break break
argv = [sys.argv[0]] + argv_ + igetopt.argv
# propagate those settings as defaults to subinterpreters, so that e.g. # propagate those settings as defaults to subinterpreters, so that e.g.
# sys.executable spawned from under `gpython -X gpython.runtime=threads` # sys.executable spawned from under `gpython -X gpython.runtime=threads`
...@@ -481,7 +491,7 @@ def main(): ...@@ -481,7 +491,7 @@ def main():
sys.version += (' [GPython %s] [runtime %s] [strings %s]' % (golang.__version__, gpy_runtime_ver, gpy_strings)) sys.version += (' [GPython %s] [runtime %s] [strings %s]' % (golang.__version__, gpy_runtime_ver, gpy_strings))
# tail to pymain # tail to pymain
pymain(argv, init) pymain(sys.argv, init)
# _is_buildout_script returns whether file @path is generated as python buildout script. # _is_buildout_script returns whether file @path is generated as python buildout script.
......
...@@ -358,6 +358,60 @@ def test_pymain_opt(): ...@@ -358,6 +358,60 @@ def test_pymain_opt():
check(["-O", "-O"]) check(["-O", "-O"])
check(["-O", "-O", "-O"]) check(["-O", "-O", "-O"])
# verify that pymain handles -E in exactly the same way as underlying python does.
@gpython_only
def test_pymain_E():
envadj = {'PYTHONOPTIMIZE': '1'}
def sys_flags_optimize(level):
return 'sys.flags.optimize: %s' % level
# without -E $PYTHONOPTIMIZE should be taken into account
def _(gpyoutv, stdpyoutv):
assert sys_flags_optimize(0) not in stdpyoutv
assert sys_flags_optimize(0) not in gpyoutv
assert sys_flags_optimize(1) in stdpyoutv
assert sys_flags_optimize(1) in gpyoutv
check_gpy_vs_py(['testprog/print_opt.py'], _, envadj=envadj, cwd=here)
# with -E not
def _(gpyoutv, stdpyoutv):
assert sys_flags_optimize(0) in stdpyoutv
assert sys_flags_optimize(0) in gpyoutv
assert sys_flags_optimize(1) not in stdpyoutv
assert sys_flags_optimize(1) not in gpyoutv
check_gpy_vs_py(['-E', 'testprog/print_opt.py'], _, envadj=envadj, cwd=here)
# verify that pymain handles -X non-gpython-option in exactly the same way as underlying python does.
@pytest.mark.skipif(PY2, reason="-X does not work at all on plain cpython2")
@gpython_only
def test_pymain_X():
check_gpy_vs_py(['testprog/print_faulthandler.py'], cwd=here)
check_gpy_vs_py(['-X', 'faulthandler', 'testprog/print_faulthandler.py'], cwd=here)
# pymain -v
@gpython_only
def test_pymain_v():
def nimport(argv, **kw):
argv = argv + ['testdata/hello.py']
kw.setdefault('cwd', here)
ret, out, err = _pyrun(argv, stdout=PIPE, stderr=PIPE, **kw)
assert ret == 0, (out, err)
n = 0
for _ in u(err).splitlines():
if _.startswith("import "):
n += 1
return n
# without -v there must be no "import ..." messages
assert nimport([]) == 0
assert nimport([], pyexe=sys._gpy_underlying_executable) == 0
# with -v there must be many "import ..." messages
assert nimport(['-v']) > 10
assert nimport(['-v'], pyexe=sys._gpy_underlying_executable) > 10
# pymain -V/--version # pymain -V/--version
# gpython_only because output differs from !gpython. # gpython_only because output differs from !gpython.
......
# -*- coding: utf-8 -*-
# Copyright (C) 2024 Nexedi SA and Contributors.
# Kirill Smelkov <kirr@nexedi.com>
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""Program print_faulthandler prints information about faulthandler settings."""
from __future__ import print_function, absolute_import
import sys
def main():
if 'faulthandler' not in sys.modules:
print('faulthandler is not imported')
return
fh = sys.modules['faulthandler']
print('faulthandler imported')
print('faulthandler %s' % ('enabled' if fh.is_enabled() else 'disabled'))
if __name__ == '__main__':
main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment