mirror of
https://github.com/msgpack/msgpack-python.git
synced 2025-10-19 20:03:16 +00:00
Update setuptools and black (#498)
* Use setuptools * Use black==22.1.0
This commit is contained in:
parent
89ea57747e
commit
cb50b2081b
13 changed files with 83 additions and 84 deletions
6
.github/workflows/black.yaml
vendored
6
.github/workflows/black.yaml
vendored
|
@ -17,9 +17,9 @@ jobs:
|
||||||
architecture: 'x64'
|
architecture: 'x64'
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Black Code Formatter
|
- name: Black Code Formatter
|
||||||
run: |
|
run: |
|
||||||
pip install black
|
pip install black==22.1.0
|
||||||
black --diff --check msgpack/ test/ setup.py
|
black -S --diff --check msgpack/ test/ setup.py
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -4,7 +4,7 @@ all: cython
|
||||||
|
|
||||||
.PHONY: black
|
.PHONY: black
|
||||||
black:
|
black:
|
||||||
black msgpack/ test/ setup.py
|
black -S msgpack/ test/ setup.py
|
||||||
|
|
||||||
.PHONY: cython
|
.PHONY: cython
|
||||||
cython:
|
cython:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from ._version import version
|
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
from .ext import ExtType, Timestamp
|
from .ext import ExtType, Timestamp
|
||||||
|
|
||||||
|
@ -7,6 +6,10 @@ import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
version = (1, 0, 4, 'dev')
|
||||||
|
__version__ = "1.0.4dev"
|
||||||
|
|
||||||
|
|
||||||
if os.environ.get("MSGPACK_PUREPYTHON") or sys.version_info[0] == 2:
|
if os.environ.get("MSGPACK_PUREPYTHON") or sys.version_info[0] == 2:
|
||||||
from .fallback import Packer, unpackb, Unpacker
|
from .fallback import Packer, unpackb, Unpacker
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
version = (1, 0, 3)
|
|
|
@ -59,7 +59,7 @@ class Timestamp(object):
|
||||||
raise TypeError("seconds must be an interger")
|
raise TypeError("seconds must be an interger")
|
||||||
if not isinstance(nanoseconds, int_types):
|
if not isinstance(nanoseconds, int_types):
|
||||||
raise TypeError("nanoseconds must be an integer")
|
raise TypeError("nanoseconds must be an integer")
|
||||||
if not (0 <= nanoseconds < 10 ** 9):
|
if not (0 <= nanoseconds < 10**9):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"nanoseconds must be a non-negative integer less than 999999999."
|
"nanoseconds must be a non-negative integer less than 999999999."
|
||||||
)
|
)
|
||||||
|
@ -143,7 +143,7 @@ class Timestamp(object):
|
||||||
:type unix_float: int or float.
|
:type unix_float: int or float.
|
||||||
"""
|
"""
|
||||||
seconds = int(unix_sec // 1)
|
seconds = int(unix_sec // 1)
|
||||||
nanoseconds = int((unix_sec % 1) * 10 ** 9)
|
nanoseconds = int((unix_sec % 1) * 10**9)
|
||||||
return Timestamp(seconds, nanoseconds)
|
return Timestamp(seconds, nanoseconds)
|
||||||
|
|
||||||
def to_unix(self):
|
def to_unix(self):
|
||||||
|
@ -161,7 +161,7 @@ class Timestamp(object):
|
||||||
:param int unix_ns: Posix timestamp in nanoseconds.
|
:param int unix_ns: Posix timestamp in nanoseconds.
|
||||||
:rtype: Timestamp
|
:rtype: Timestamp
|
||||||
"""
|
"""
|
||||||
return Timestamp(*divmod(unix_ns, 10 ** 9))
|
return Timestamp(*divmod(unix_ns, 10**9))
|
||||||
|
|
||||||
def to_unix_nano(self):
|
def to_unix_nano(self):
|
||||||
"""Get the timestamp as a unixtime in nanoseconds.
|
"""Get the timestamp as a unixtime in nanoseconds.
|
||||||
|
@ -169,7 +169,7 @@ class Timestamp(object):
|
||||||
:returns: posix timestamp in nanoseconds
|
:returns: posix timestamp in nanoseconds
|
||||||
:rtype: int
|
:rtype: int
|
||||||
"""
|
"""
|
||||||
return self.seconds * 10 ** 9 + self.nanoseconds
|
return self.seconds * 10**9 + self.nanoseconds
|
||||||
|
|
||||||
def to_datetime(self):
|
def to_datetime(self):
|
||||||
"""Get the timestamp as a UTC datetime.
|
"""Get the timestamp as a UTC datetime.
|
||||||
|
|
|
@ -318,7 +318,7 @@ class Unpacker(object):
|
||||||
self._buf_checkpoint = 0
|
self._buf_checkpoint = 0
|
||||||
|
|
||||||
if not max_buffer_size:
|
if not max_buffer_size:
|
||||||
max_buffer_size = 2 ** 31 - 1
|
max_buffer_size = 2**31 - 1
|
||||||
if max_str_len == -1:
|
if max_str_len == -1:
|
||||||
max_str_len = max_buffer_size
|
max_str_len = max_buffer_size
|
||||||
if max_bin_len == -1:
|
if max_bin_len == -1:
|
||||||
|
@ -800,20 +800,20 @@ class Packer(object):
|
||||||
raise OverflowError("Integer value out of range")
|
raise OverflowError("Integer value out of range")
|
||||||
if check(obj, (bytes, bytearray)):
|
if check(obj, (bytes, bytearray)):
|
||||||
n = len(obj)
|
n = len(obj)
|
||||||
if n >= 2 ** 32:
|
if n >= 2**32:
|
||||||
raise ValueError("%s is too large" % type(obj).__name__)
|
raise ValueError("%s is too large" % type(obj).__name__)
|
||||||
self._pack_bin_header(n)
|
self._pack_bin_header(n)
|
||||||
return self._buffer.write(obj)
|
return self._buffer.write(obj)
|
||||||
if check(obj, unicode):
|
if check(obj, unicode):
|
||||||
obj = obj.encode("utf-8", self._unicode_errors)
|
obj = obj.encode("utf-8", self._unicode_errors)
|
||||||
n = len(obj)
|
n = len(obj)
|
||||||
if n >= 2 ** 32:
|
if n >= 2**32:
|
||||||
raise ValueError("String is too large")
|
raise ValueError("String is too large")
|
||||||
self._pack_raw_header(n)
|
self._pack_raw_header(n)
|
||||||
return self._buffer.write(obj)
|
return self._buffer.write(obj)
|
||||||
if check(obj, memoryview):
|
if check(obj, memoryview):
|
||||||
n = len(obj) * obj.itemsize
|
n = len(obj) * obj.itemsize
|
||||||
if n >= 2 ** 32:
|
if n >= 2**32:
|
||||||
raise ValueError("Memoryview is too large")
|
raise ValueError("Memoryview is too large")
|
||||||
self._pack_bin_header(n)
|
self._pack_bin_header(n)
|
||||||
return self._buffer.write(obj)
|
return self._buffer.write(obj)
|
||||||
|
@ -895,7 +895,7 @@ class Packer(object):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def pack_array_header(self, n):
|
def pack_array_header(self, n):
|
||||||
if n >= 2 ** 32:
|
if n >= 2**32:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
self._pack_array_header(n)
|
self._pack_array_header(n)
|
||||||
if self._autoreset:
|
if self._autoreset:
|
||||||
|
@ -904,7 +904,7 @@ class Packer(object):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def pack_map_header(self, n):
|
def pack_map_header(self, n):
|
||||||
if n >= 2 ** 32:
|
if n >= 2**32:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
self._pack_map_header(n)
|
self._pack_map_header(n)
|
||||||
if self._autoreset:
|
if self._autoreset:
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
# Also declared in pyproject.toml, if updating here please also update there
|
# Also declared in pyproject.toml, if updating here please also update there
|
||||||
Cython~=0.29.13
|
Cython~=0.29.13
|
||||||
|
|
||||||
|
# dev only tools. no need to add pyproject
|
||||||
|
black==22.1.0
|
||||||
|
|
32
setup.cfg
Normal file
32
setup.cfg
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
[metadata]
|
||||||
|
name = msgpack
|
||||||
|
#version = attr: msgpack.__version__
|
||||||
|
version = attr: msgpack.version
|
||||||
|
license = Apache 2.0
|
||||||
|
author = Inada Naoki
|
||||||
|
author_email = songofacandy@gmail.com
|
||||||
|
description = MessagePack serializer
|
||||||
|
long_description = file: README.md
|
||||||
|
long_description_content_type = text/markdown
|
||||||
|
url = https://msgpack.org/
|
||||||
|
|
||||||
|
project_urls =
|
||||||
|
Documentation = https://msgpack-python.readthedocs.io/
|
||||||
|
Source = https://github.com/msgpack/msgpack-python
|
||||||
|
Tracker = https://github.com/msgpack/msgpack-python/issues
|
||||||
|
|
||||||
|
classifiers =
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
Programming Language :: Python :: 3.6
|
||||||
|
Programming Language :: Python :: 3.7
|
||||||
|
Programming Language :: Python :: 3.8
|
||||||
|
Programming Language :: Python :: 3.9
|
||||||
|
Programming Language :: Python :: 3.10
|
||||||
|
Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Intended Audience :: Developers
|
||||||
|
License :: OSI Approved :: Apache Software License
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
max_line_length = 100
|
||||||
|
|
42
setup.py
42
setup.py
|
@ -4,10 +4,9 @@ import io
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from distutils.command.sdist import sdist
|
|
||||||
from setuptools import setup, Extension
|
from setuptools import setup, Extension
|
||||||
|
from setuptools.command.build_ext import build_ext
|
||||||
from distutils.command.build_ext import build_ext
|
from setuptools.command.sdist import sdist
|
||||||
|
|
||||||
|
|
||||||
PYPY = hasattr(sys, "pypy_version_info")
|
PYPY = hasattr(sys, "pypy_version_info")
|
||||||
|
@ -65,12 +64,6 @@ class BuildExt(build_ext):
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
exec(open("msgpack/_version.py").read())
|
|
||||||
|
|
||||||
version_str = ".".join(str(x) for x in version[:3])
|
|
||||||
if len(version) > 3 and version[3] != "final":
|
|
||||||
version_str += version[3]
|
|
||||||
|
|
||||||
# Cython is required for sdist
|
# Cython is required for sdist
|
||||||
class Sdist(sdist):
|
class Sdist(sdist):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -99,39 +92,8 @@ if not PYPY and not PY2 and not os.environ.get("MSGPACK_PUREPYTHON"):
|
||||||
del libraries, macros
|
del libraries, macros
|
||||||
|
|
||||||
|
|
||||||
desc = "MessagePack (de)serializer."
|
|
||||||
with io.open("README.md", encoding="utf-8") as f:
|
|
||||||
long_desc = f.read()
|
|
||||||
del f
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="msgpack",
|
|
||||||
author="Inada Naoki",
|
|
||||||
author_email="songofacandy@gmail.com",
|
|
||||||
version=version_str,
|
|
||||||
cmdclass={"build_ext": BuildExt, "sdist": Sdist},
|
cmdclass={"build_ext": BuildExt, "sdist": Sdist},
|
||||||
ext_modules=ext_modules,
|
ext_modules=ext_modules,
|
||||||
packages=["msgpack"],
|
packages=["msgpack"],
|
||||||
description=desc,
|
|
||||||
long_description=long_desc,
|
|
||||||
long_description_content_type="text/markdown",
|
|
||||||
url="https://msgpack.org/",
|
|
||||||
project_urls={
|
|
||||||
"Documentation": "https://msgpack-python.readthedocs.io/",
|
|
||||||
"Source": "https://github.com/msgpack/msgpack-python",
|
|
||||||
"Tracker": "https://github.com/msgpack/msgpack-python/issues",
|
|
||||||
},
|
|
||||||
license="Apache 2.0",
|
|
||||||
classifiers=[
|
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
"Programming Language :: Python :: 3.6",
|
|
||||||
"Programming Language :: Python :: 3.7",
|
|
||||||
"Programming Language :: Python :: 3.8",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
|
||||||
"Intended Audience :: Developers",
|
|
||||||
"License :: OSI Approved :: Apache Software License",
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,12 +16,12 @@ from msgpack import (
|
||||||
|
|
||||||
|
|
||||||
def test_integer():
|
def test_integer():
|
||||||
x = -(2 ** 63)
|
x = -(2**63)
|
||||||
assert unpackb(packb(x)) == x
|
assert unpackb(packb(x)) == x
|
||||||
with pytest.raises(PackOverflowError):
|
with pytest.raises(PackOverflowError):
|
||||||
packb(x - 1)
|
packb(x - 1)
|
||||||
|
|
||||||
x = 2 ** 64 - 1
|
x = 2**64 - 1
|
||||||
assert unpackb(packb(x)) == x
|
assert unpackb(packb(x)) == x
|
||||||
with pytest.raises(PackOverflowError):
|
with pytest.raises(PackOverflowError):
|
||||||
packb(x + 1)
|
packb(x + 1)
|
||||||
|
@ -29,16 +29,16 @@ def test_integer():
|
||||||
|
|
||||||
def test_array_header():
|
def test_array_header():
|
||||||
packer = Packer()
|
packer = Packer()
|
||||||
packer.pack_array_header(2 ** 32 - 1)
|
packer.pack_array_header(2**32 - 1)
|
||||||
with pytest.raises(PackValueError):
|
with pytest.raises(PackValueError):
|
||||||
packer.pack_array_header(2 ** 32)
|
packer.pack_array_header(2**32)
|
||||||
|
|
||||||
|
|
||||||
def test_map_header():
|
def test_map_header():
|
||||||
packer = Packer()
|
packer = Packer()
|
||||||
packer.pack_map_header(2 ** 32 - 1)
|
packer.pack_map_header(2**32 - 1)
|
||||||
with pytest.raises(PackValueError):
|
with pytest.raises(PackValueError):
|
||||||
packer.pack_array_header(2 ** 32)
|
packer.pack_array_header(2**32)
|
||||||
|
|
||||||
|
|
||||||
def test_max_str_len():
|
def test_max_str_len():
|
||||||
|
|
|
@ -53,46 +53,46 @@ def test_fixstr_from_float():
|
||||||
|
|
||||||
|
|
||||||
def test_str16_from_byte():
|
def test_str16_from_byte():
|
||||||
_runtest("B", 2 ** 8, b"\xda", b"\x01\x00", False)
|
_runtest("B", 2**8, b"\xda", b"\x01\x00", False)
|
||||||
_runtest("B", 2 ** 16 - 1, b"\xda", b"\xff\xff", False)
|
_runtest("B", 2**16 - 1, b"\xda", b"\xff\xff", False)
|
||||||
|
|
||||||
|
|
||||||
def test_str16_from_float():
|
def test_str16_from_float():
|
||||||
_runtest("f", 2 ** 8, b"\xda", b"\x01\x00", False)
|
_runtest("f", 2**8, b"\xda", b"\x01\x00", False)
|
||||||
_runtest("f", 2 ** 16 - 4, b"\xda", b"\xff\xfc", False)
|
_runtest("f", 2**16 - 4, b"\xda", b"\xff\xfc", False)
|
||||||
|
|
||||||
|
|
||||||
def test_str32_from_byte():
|
def test_str32_from_byte():
|
||||||
_runtest("B", 2 ** 16, b"\xdb", b"\x00\x01\x00\x00", False)
|
_runtest("B", 2**16, b"\xdb", b"\x00\x01\x00\x00", False)
|
||||||
|
|
||||||
|
|
||||||
def test_str32_from_float():
|
def test_str32_from_float():
|
||||||
_runtest("f", 2 ** 16, b"\xdb", b"\x00\x01\x00\x00", False)
|
_runtest("f", 2**16, b"\xdb", b"\x00\x01\x00\x00", False)
|
||||||
|
|
||||||
|
|
||||||
def test_bin8_from_byte():
|
def test_bin8_from_byte():
|
||||||
_runtest("B", 1, b"\xc4", b"\x01", True)
|
_runtest("B", 1, b"\xc4", b"\x01", True)
|
||||||
_runtest("B", 2 ** 8 - 1, b"\xc4", b"\xff", True)
|
_runtest("B", 2**8 - 1, b"\xc4", b"\xff", True)
|
||||||
|
|
||||||
|
|
||||||
def test_bin8_from_float():
|
def test_bin8_from_float():
|
||||||
_runtest("f", 4, b"\xc4", b"\x04", True)
|
_runtest("f", 4, b"\xc4", b"\x04", True)
|
||||||
_runtest("f", 2 ** 8 - 4, b"\xc4", b"\xfc", True)
|
_runtest("f", 2**8 - 4, b"\xc4", b"\xfc", True)
|
||||||
|
|
||||||
|
|
||||||
def test_bin16_from_byte():
|
def test_bin16_from_byte():
|
||||||
_runtest("B", 2 ** 8, b"\xc5", b"\x01\x00", True)
|
_runtest("B", 2**8, b"\xc5", b"\x01\x00", True)
|
||||||
_runtest("B", 2 ** 16 - 1, b"\xc5", b"\xff\xff", True)
|
_runtest("B", 2**16 - 1, b"\xc5", b"\xff\xff", True)
|
||||||
|
|
||||||
|
|
||||||
def test_bin16_from_float():
|
def test_bin16_from_float():
|
||||||
_runtest("f", 2 ** 8, b"\xc5", b"\x01\x00", True)
|
_runtest("f", 2**8, b"\xc5", b"\x01\x00", True)
|
||||||
_runtest("f", 2 ** 16 - 4, b"\xc5", b"\xff\xfc", True)
|
_runtest("f", 2**16 - 4, b"\xc5", b"\xff\xfc", True)
|
||||||
|
|
||||||
|
|
||||||
def test_bin32_from_byte():
|
def test_bin32_from_byte():
|
||||||
_runtest("B", 2 ** 16, b"\xc6", b"\x00\x01\x00\x00", True)
|
_runtest("B", 2**16, b"\xc6", b"\x00\x01\x00\x00", True)
|
||||||
|
|
||||||
|
|
||||||
def test_bin32_from_float():
|
def test_bin32_from_float():
|
||||||
_runtest("f", 2 ** 16, b"\xc6", b"\x00\x01\x00\x00", True)
|
_runtest("f", 2**16, b"\xc6", b"\x00\x01\x00\x00", True)
|
||||||
|
|
|
@ -118,8 +118,8 @@ def test_issue124():
|
||||||
|
|
||||||
def test_unpack_tell():
|
def test_unpack_tell():
|
||||||
stream = io.BytesIO()
|
stream = io.BytesIO()
|
||||||
messages = [2 ** i - 1 for i in range(65)]
|
messages = [2**i - 1 for i in range(65)]
|
||||||
messages += [-(2 ** i) for i in range(1, 64)]
|
messages += [-(2**i) for i in range(1, 64)]
|
||||||
messages += [
|
messages += [
|
||||||
b"hello",
|
b"hello",
|
||||||
b"hello" * 1000,
|
b"hello" * 1000,
|
||||||
|
|
|
@ -10,31 +10,31 @@ if sys.version_info[0] > 2:
|
||||||
|
|
||||||
def test_timestamp():
|
def test_timestamp():
|
||||||
# timestamp32
|
# timestamp32
|
||||||
ts = Timestamp(2 ** 32 - 1)
|
ts = Timestamp(2**32 - 1)
|
||||||
assert ts.to_bytes() == b"\xff\xff\xff\xff"
|
assert ts.to_bytes() == b"\xff\xff\xff\xff"
|
||||||
packed = msgpack.packb(ts)
|
packed = msgpack.packb(ts)
|
||||||
assert packed == b"\xd6\xff" + ts.to_bytes()
|
assert packed == b"\xd6\xff" + ts.to_bytes()
|
||||||
unpacked = msgpack.unpackb(packed)
|
unpacked = msgpack.unpackb(packed)
|
||||||
assert ts == unpacked
|
assert ts == unpacked
|
||||||
assert ts.seconds == 2 ** 32 - 1 and ts.nanoseconds == 0
|
assert ts.seconds == 2**32 - 1 and ts.nanoseconds == 0
|
||||||
|
|
||||||
# timestamp64
|
# timestamp64
|
||||||
ts = Timestamp(2 ** 34 - 1, 999999999)
|
ts = Timestamp(2**34 - 1, 999999999)
|
||||||
assert ts.to_bytes() == b"\xee\x6b\x27\xff\xff\xff\xff\xff"
|
assert ts.to_bytes() == b"\xee\x6b\x27\xff\xff\xff\xff\xff"
|
||||||
packed = msgpack.packb(ts)
|
packed = msgpack.packb(ts)
|
||||||
assert packed == b"\xd7\xff" + ts.to_bytes()
|
assert packed == b"\xd7\xff" + ts.to_bytes()
|
||||||
unpacked = msgpack.unpackb(packed)
|
unpacked = msgpack.unpackb(packed)
|
||||||
assert ts == unpacked
|
assert ts == unpacked
|
||||||
assert ts.seconds == 2 ** 34 - 1 and ts.nanoseconds == 999999999
|
assert ts.seconds == 2**34 - 1 and ts.nanoseconds == 999999999
|
||||||
|
|
||||||
# timestamp96
|
# timestamp96
|
||||||
ts = Timestamp(2 ** 63 - 1, 999999999)
|
ts = Timestamp(2**63 - 1, 999999999)
|
||||||
assert ts.to_bytes() == b"\x3b\x9a\xc9\xff\x7f\xff\xff\xff\xff\xff\xff\xff"
|
assert ts.to_bytes() == b"\x3b\x9a\xc9\xff\x7f\xff\xff\xff\xff\xff\xff\xff"
|
||||||
packed = msgpack.packb(ts)
|
packed = msgpack.packb(ts)
|
||||||
assert packed == b"\xc7\x0c\xff" + ts.to_bytes()
|
assert packed == b"\xc7\x0c\xff" + ts.to_bytes()
|
||||||
unpacked = msgpack.unpackb(packed)
|
unpacked = msgpack.unpackb(packed)
|
||||||
assert ts == unpacked
|
assert ts == unpacked
|
||||||
assert ts.seconds == 2 ** 63 - 1 and ts.nanoseconds == 999999999
|
assert ts.seconds == 2**63 - 1 and ts.nanoseconds == 999999999
|
||||||
|
|
||||||
# negative fractional
|
# negative fractional
|
||||||
ts = Timestamp.from_unix(-2.3) # s: -3, ns: 700000000
|
ts = Timestamp.from_unix(-2.3) # s: -3, ns: 700000000
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue