From 804968a6b3713c15554baf8ccfbe393478a3a0b5 Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 30 Jul 2024 16:14:13 -0400 Subject: [PATCH 1/4] resolve verstamp bootstraping problem --- .github/workflows/main.yml | 4 +- README.md | 2 +- mypy.ini | 3 +- setup.py | 49 ++--- win32/Lib/_win32verstamp_pywin32ctypes.py | 207 ++++++++++++++++++++++ win32/Lib/win32verstamp.py | 9 +- win32/scripts/VersionStamp/bulkstamp.py | 39 ++-- 7 files changed, 254 insertions(+), 59 deletions(-) create mode 100644 win32/Lib/_win32verstamp_pywin32ctypes.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bde655f3b..258cdd2c5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,7 +40,7 @@ jobs: - name: Build and install run: | - python setup.py --skip-verstamp install --user + python setup.py install --user - name: Run tests # Run the tests directly from the source dir so support files (eg, .wav files etc) @@ -91,7 +91,7 @@ jobs: python .github\workflows\download-arm64-libs.py .\arm64libs - name: Build wheels - run: python setup.py --skip-verstamp build_ext -L .\arm64libs --plat-name win-arm64 build --plat-name win-arm64 bdist_wheel --plat-name win-arm64 + run: python setup.py build_ext -L .\arm64libs --plat-name win-arm64 build --plat-name win-arm64 bdist_wheel --plat-name win-arm64 - uses: actions/upload-artifact@v3 if: ${{ always() }} diff --git a/README.md b/README.md index d8e258db9..495cf8c5c 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ to form a checklist so @mhammond doesn't forget what to do :) * Update setup.py with the new build number. -* Execute `make.bat`, wait forever, test the artifacts. +* Execute `make_all.bat`, wait forever, test the artifacts. * Upload .whl artifacts to pypi - we do this before pushing the tag because they might be rejected for an invalid `README.md`. Done via `py -3.? -m twine upload dist/*XXX*.whl`. diff --git a/mypy.ini b/mypy.ini index a93198a2f..f2d5ea44a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -48,11 +48,10 @@ exclude = (?x)( [mypy-adsi.*,dde,exchange,exchdapi,mapi,perfmon,servicemanager,win32api,win32console,win32clipboard,win32comext.adsi.adsi,win32event,win32evtlog,win32file,win32gui,win32help,win32pdh,win32process,win32ras,win32security,win32service,win32trace,win32ui,win32uiole,win32wnet,_win32sysloader,_winxptheme] ignore_missing_imports = True -; verstamp is installed from win32verstamp.py called in setup.py ; Most of win32com re-exports win32comext ; Test is a local untyped module in win32comext.axdebug ; pywin32_system32 is an empty module created in setup.py to store dlls -[mypy-verstamp,win32com.*,Test,pywin32_system32] +[mypy-win32com.*,Test,pywin32_system32] ignore_missing_imports = True ; Distutils being removed from stdlib currently causes some issues on Python 3.12 diff --git a/setup.py b/setup.py index 6ce1ed77d..1132c2cc5 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ __doc__ = """This is a distutils setup-script for the pywin32 extensions. The canonical source of truth for supported versions and build environments -is [the github CI](https://github.com/mhammond/pywin32/tree/main/.github/workflows). +is [the GitHub CI](https://github.com/mhammond/pywin32/tree/main/.github/workflows). To build and install locally for testing etc, you need a build environment which is capable of building the version of Python you are targeting, then: @@ -62,12 +62,6 @@ ) print("Building pywin32", pywin32_version) -try: - sys.argv.remove("--skip-verstamp") - skip_verstamp = True -except ValueError: - skip_verstamp = False - try: this_file = __file__ except NameError: @@ -985,35 +979,18 @@ def link( # target. Do this externally to avoid suddenly dragging in the # modules needed by this process, and which we will soon try and # update. - # Further, we don't really want to use sys.executable, because that - # means the build environment must have a current pywin32 installed - # in every version, which is a bit of a burden only for this. - # So we assume the "default" Python version (ie, the version run by - # py.exe) has pywin32 installed. - # (This creates a chicken-and-egg problem though! We used to work around - # this by ignoring failure to verstamp, but that's easy to miss. So now - # allow --skip-verstamp on the cmdline - but if it's not there, the - # verstamp must work.) - if not skip_verstamp: - args = ["py.exe", "-m", "win32verstamp"] - args.append(f"--version={pywin32_version}") - args.append("--comments=https://github.com/mhammond/pywin32") - args.append(f"--original-filename={os.path.basename(output_filename)}") - args.append("--product=PyWin32") - if "-v" not in sys.argv: - args.append("--quiet") - args.append(output_filename) - try: - self.spawn(args) - except Exception: - print("** Failed to versionstamp the binaries.") - # py.exe is not yet available for windows-arm64 so version stamp will fail - # ignore it for now - if platform.machine() != "ARM64": - print( - "** If you want to skip this step, pass '--skip-verstamp' on the setup.py command-line" - ) - raise + args = [ + sys.executable, + # NOTE: On Python 3.7, all args must be str + str(Path(__file__).parent / "win32" / "Lib" / "win32verstamp.py"), + f"--version={pywin32_version}", + "--comments=https://github.com/mhammond/pywin32", + f"--original-filename={os.path.basename(output_filename)}", + "--product=PyWin32", + "--quiet" if "-v" not in sys.argv else "", + output_filename, + ] + self.spawn(args) # Work around bpo-36302/bpo-42009 - it sorts sources but this breaks # support for building .mc files etc :( diff --git a/win32/Lib/_win32verstamp_pywin32ctypes.py b/win32/Lib/_win32verstamp_pywin32ctypes.py new file mode 100644 index 000000000..3ad5ca344 --- /dev/null +++ b/win32/Lib/_win32verstamp_pywin32ctypes.py @@ -0,0 +1,207 @@ +""" +A pure-python re-implementation of methods used by win32verstamp. +This is to avoid a bootstraping problem where win32verstamp is used during build, +but requires an installation of pywin32 to be present. +We used to work around this by ignoring failure to verstamp, but that's easy to miss. + +Implementations adapted, simplified and typed from: +- https://github.com/enthought/pywin32-ctypes/blob/main/win32ctypes/core/ctypes/_util.py +- https://github.com/enthought/pywin32-ctypes/blob/main/win32ctypes/core/cffi/_resource.py +- https://github.com/enthought/pywin32-ctypes/blob/main/win32ctypes/pywin32/win32api.py + +--- + +(C) Copyright 2014 Enthought, Inc., Austin, TX +All right reserved. + +This file is open source software distributed according to the terms in +https://github.com/enthought/pywin32-ctypes/blob/main/LICENSE.txt +""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable +from ctypes import FormatError, WinDLL, _SimpleCData, get_last_error +from ctypes.wintypes import ( + BOOL, + DWORD, + HANDLE, + LPCWSTR, + LPVOID, + WORD, +) +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from ctypes import _NamedFuncPointer + + from _typeshed import ReadableBuffer + from typing_extensions import Literal, SupportsBytes, SupportsIndex + +kernel32 = WinDLL("kernel32", use_last_error=True) + +### +# https://github.com/enthought/pywin32-ctypes/blob/main/win32ctypes/core/ctypes/_util.py +### + + +def function_factory( + function: _NamedFuncPointer, + argument_types: list[type[_SimpleCData[Any]]], + return_type: type[_SimpleCData[Any]], + error_checking: Callable[..., Any], # Simplified over errcheck's signature +) -> _NamedFuncPointer: + function.argtypes = argument_types + function.restype = return_type + function.errcheck = error_checking + return function + + +def make_error(function: _NamedFuncPointer) -> OSError: + code = get_last_error() + description = FormatError(code).strip() + function_name = function.__name__ + exception = OSError() + exception.winerror = code + exception.function = function_name + exception.strerror = description + return exception + + +def check_null(result: int | None, function: _NamedFuncPointer, *_) -> int: + if result is None: + raise make_error(function) + return result + + +def check_false(result: int | None, function: _NamedFuncPointer, *_) -> Literal[True]: + if not bool(result): + raise make_error(function) + else: + return True + + +### +# https://github.com/enthought/pywin32-ctypes/blob/main/win32ctypes/core/cffi/_resource.py +### + + +def _UpdateResource( + hUpdate: int, + lpType: str | int, + lpName: str | int, + wLanguage: int, + lpData: bytes, + cbData: int, +): + lp_type = LPCWSTR(lpType) + lp_name = LPCWSTR(lpName) + _BaseUpdateResource(hUpdate, lp_type, lp_name, wLanguage, lpData, cbData) + + +_BeginUpdateResource = function_factory( + kernel32.BeginUpdateResourceW, + [LPCWSTR, BOOL], + HANDLE, + check_null, +) + + +_EndUpdateResource = function_factory( + kernel32.EndUpdateResourceW, + [HANDLE, BOOL], + BOOL, + check_false, +) + +_BaseUpdateResource = function_factory( + kernel32.UpdateResourceW, + [HANDLE, LPCWSTR, LPCWSTR, WORD, LPVOID, DWORD], + BOOL, + check_false, +) + + +### +# https://github.com/enthought/pywin32-ctypes/blob/main/win32ctypes/pywin32/win32api.py +### + +LANG_NEUTRAL = 0x00 + + +def BeginUpdateResource(filename: str, delete: bool): + """Get a handle that can be used by the :func:`UpdateResource`. + + Parameters + ---------- + fileName : unicode + The filename of the module to load. + delete : bool + When true all existing resources are deleted + + Returns + ------- + result : hModule + Handle of the resource. + + """ + return _BeginUpdateResource(filename, delete) + + +def EndUpdateResource(handle: int, discard: bool) -> None: + """End the update resource of the handle. + + Parameters + ---------- + handle : hModule + The handle of the resource as it is returned + by :func:`BeginUpdateResource` + + discard : bool + When True all writes are discarded. + + """ + _EndUpdateResource(handle, discard) + + +def UpdateResource( + handle: int, + type: str | int, + name: str | int, + data: Iterable[SupportsIndex] | SupportsIndex | SupportsBytes | ReadableBuffer, + language=LANG_NEUTRAL, +) -> None: + """Update a resource. + + Parameters + ---------- + handle : hModule + The handle of the resource file as returned by + :func:`BeginUpdateResource`. + + type : str : int + The type of resource to update. + + name : str : int + The name or Id of the resource to update. + + data : bytes + A bytes like object is expected. + + .. note:: + PyWin32 version 219, on Python 2.7, can handle unicode inputs. + However, the data are stored as bytes and it is not really + possible to convert the information back into the original + unicode string. To be consistent with the Python 3 behaviour + of PyWin32, we raise an error if the input cannot be + converted to `bytes`. + + language : int + Language to use, default is LANG_NEUTRAL. + + """ + try: + lp_data = bytes(data) + except UnicodeEncodeError: + raise TypeError("a bytes-like object is required, not a 'unicode'") + _UpdateResource(handle, type, name, language, lp_data, len(lp_data)) diff --git a/win32/Lib/win32verstamp.py b/win32/Lib/win32verstamp.py index e9f8c5e45..7b211303d 100644 --- a/win32/Lib/win32verstamp.py +++ b/win32/Lib/win32verstamp.py @@ -1,12 +1,15 @@ -""" Stamp a Win32 binary with version information. -""" +"""Stamp a Win32 binary with version information.""" import glob import optparse import os import struct -from win32api import BeginUpdateResource, EndUpdateResource, UpdateResource +from _win32verstamp_pywin32ctypes import ( + BeginUpdateResource, + EndUpdateResource, + UpdateResource, +) VS_FFI_SIGNATURE = -17890115 # 0xFEEF04BD VS_FFI_STRUCVERSION = 0x00010000 diff --git a/win32/scripts/VersionStamp/bulkstamp.py b/win32/scripts/VersionStamp/bulkstamp.py index 3a039ebb8..49bdc3e93 100644 --- a/win32/scripts/VersionStamp/bulkstamp.py +++ b/win32/scripts/VersionStamp/bulkstamp.py @@ -33,9 +33,15 @@ import fnmatch import os import sys +from collections.abc import Mapping +from optparse import Values -import verstamp -import win32api +try: + import win32verstamp +except ModuleNotFoundError: + # If run with pywin32 not already installed + sys.path.append(os.path.abspath(__file__ + "/../../../Lib")) + import win32verstamp numStamped = 0 @@ -47,9 +53,8 @@ ] -def walk(arg, dirname, names): +def walk(vars: Mapping[str, str], debug, descriptions, dirname, names): global numStamped - vars, debug, descriptions = arg for name in names: for pat in g_patterns: if fnmatch.fnmatch(name, pat): @@ -60,18 +65,21 @@ def walk(arg, dirname, names): name = base[:-2] + ext is_dll = ext.lower() != ".exe" if os.path.normcase(name) in descriptions: - desc = descriptions[os.path.normcase(name)] + description = descriptions[os.path.normcase(name)] try: - verstamp.stamp(vars, pathname, desc, is_dll=is_dll) + options = Values( + {**vars, "description": description, "dll": is_dll} + ) + win32verstamp.stamp(pathname, options) numStamped += 1 - except win32api.error as exc: + except Exception as exc: print( "Could not stamp", pathname, - "Error", - exc.winerror, + "with", + options, "-", - exc.strerror, + repr(exc), ) else: print("WARNING: description not provided for:", name) @@ -82,7 +90,7 @@ def walk(arg, dirname, names): def load_descriptions(fname, vars): - retvars = {} + retvars: dict[str, str] = {} descriptions = {} lines = open(fname, "r").readlines() @@ -118,7 +126,8 @@ def load_descriptions(fname, vars): return retvars, descriptions -def scan(build, root, desc, **custom_vars): +def scan(build, root: str, desc, **custom_vars): + print(build, root, desc) global numStamped numStamped = 0 try: @@ -135,8 +144,8 @@ def scan(build, root, desc, **custom_vars): vars["build"] = build vars.update(custom_vars) - arg = vars, debug, descriptions - os.path.walk(root, walk, arg) + for directory, dirnames, filenames in os.walk(root): + walk(vars, debug, descriptions, directory, filenames) print("Stamped %d files." % (numStamped)) @@ -146,4 +155,4 @@ def scan(build, root, desc, **custom_vars): print("ERROR: incorrect invocation. See script's header comments.") sys.exit(1) - scan(*tuple(sys.argv[1:])) + scan(*sys.argv[1:]) From 24b5d41e5967db73416c8976d9bdcb2402b839f5 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 1 Aug 2024 23:25:32 -0400 Subject: [PATCH 2/4] Simplify further --- win32/Lib/_win32verstamp_pywin32ctypes.py | 95 +++++++---------------- win32/scripts/VersionStamp/bulkstamp.py | 8 +- 2 files changed, 30 insertions(+), 73 deletions(-) diff --git a/win32/Lib/_win32verstamp_pywin32ctypes.py b/win32/Lib/_win32verstamp_pywin32ctypes.py index 3ad5ca344..8b151d94c 100644 --- a/win32/Lib/_win32verstamp_pywin32ctypes.py +++ b/win32/Lib/_win32verstamp_pywin32ctypes.py @@ -20,8 +20,8 @@ from __future__ import annotations -from collections.abc import Callable, Iterable -from ctypes import FormatError, WinDLL, _SimpleCData, get_last_error +from collections.abc import Iterable +from ctypes import FormatError, WinDLL, get_last_error from ctypes.wintypes import ( BOOL, DWORD, @@ -30,7 +30,7 @@ LPVOID, WORD, ) -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING if TYPE_CHECKING: from ctypes import _NamedFuncPointer @@ -45,26 +45,12 @@ ### -def function_factory( - function: _NamedFuncPointer, - argument_types: list[type[_SimpleCData[Any]]], - return_type: type[_SimpleCData[Any]], - error_checking: Callable[..., Any], # Simplified over errcheck's signature -) -> _NamedFuncPointer: - function.argtypes = argument_types - function.restype = return_type - function.errcheck = error_checking - return function - - def make_error(function: _NamedFuncPointer) -> OSError: code = get_last_error() - description = FormatError(code).strip() - function_name = function.__name__ exception = OSError() exception.winerror = code - exception.function = function_name - exception.strerror = description + exception.function = function.__name__ + exception.strerror = FormatError(code).strip() return exception @@ -85,41 +71,21 @@ def check_false(result: int | None, function: _NamedFuncPointer, *_) -> Literal[ # https://github.com/enthought/pywin32-ctypes/blob/main/win32ctypes/core/cffi/_resource.py ### - -def _UpdateResource( - hUpdate: int, - lpType: str | int, - lpName: str | int, - wLanguage: int, - lpData: bytes, - cbData: int, -): - lp_type = LPCWSTR(lpType) - lp_name = LPCWSTR(lpName) - _BaseUpdateResource(hUpdate, lp_type, lp_name, wLanguage, lpData, cbData) - - -_BeginUpdateResource = function_factory( - kernel32.BeginUpdateResourceW, - [LPCWSTR, BOOL], - HANDLE, - check_null, -) +_BeginUpdateResource = kernel32.BeginUpdateResourceW +_BeginUpdateResource.argtypes = [LPCWSTR, BOOL] +_BeginUpdateResource.restype = HANDLE +_BeginUpdateResource.errcheck = check_null -_EndUpdateResource = function_factory( - kernel32.EndUpdateResourceW, - [HANDLE, BOOL], - BOOL, - check_false, -) +_EndUpdateResource = kernel32.EndUpdateResourceW +_EndUpdateResource.argtypes = [HANDLE, BOOL] +_EndUpdateResource.restype = BOOL +_EndUpdateResource.errcheck = check_false -_BaseUpdateResource = function_factory( - kernel32.UpdateResourceW, - [HANDLE, LPCWSTR, LPCWSTR, WORD, LPVOID, DWORD], - BOOL, - check_false, -) +_UpdateResource = kernel32.UpdateResourceW +_UpdateResource.argtypes = [HANDLE, LPCWSTR, LPCWSTR, WORD, LPVOID, DWORD] +_UpdateResource.restype = BOOL +_UpdateResource.errcheck = check_false ### @@ -134,7 +100,7 @@ def BeginUpdateResource(filename: str, delete: bool): Parameters ---------- - fileName : unicode + fileName : str The filename of the module to load. delete : bool When true all existing resources are deleted @@ -169,7 +135,7 @@ def UpdateResource( type: str | int, name: str | int, data: Iterable[SupportsIndex] | SupportsIndex | SupportsBytes | ReadableBuffer, - language=LANG_NEUTRAL, + language: int = LANG_NEUTRAL, ) -> None: """Update a resource. @@ -179,29 +145,20 @@ def UpdateResource( The handle of the resource file as returned by :func:`BeginUpdateResource`. - type : str : int + type : str | int The type of resource to update. - name : str : int + name : str | int The name or Id of the resource to update. - data : bytes + data : bytes-like A bytes like object is expected. - .. note:: - PyWin32 version 219, on Python 2.7, can handle unicode inputs. - However, the data are stored as bytes and it is not really - possible to convert the information back into the original - unicode string. To be consistent with the Python 3 behaviour - of PyWin32, we raise an error if the input cannot be - converted to `bytes`. - language : int Language to use, default is LANG_NEUTRAL. """ - try: - lp_data = bytes(data) - except UnicodeEncodeError: - raise TypeError("a bytes-like object is required, not a 'unicode'") - _UpdateResource(handle, type, name, language, lp_data, len(lp_data)) + lp_data = bytes(data) + _UpdateResource( + handle, LPCWSTR(type), LPCWSTR(name), language, lp_data, len(lp_data) + ) diff --git a/win32/scripts/VersionStamp/bulkstamp.py b/win32/scripts/VersionStamp/bulkstamp.py index 49bdc3e93..cd8619243 100644 --- a/win32/scripts/VersionStamp/bulkstamp.py +++ b/win32/scripts/VersionStamp/bulkstamp.py @@ -72,14 +72,14 @@ def walk(vars: Mapping[str, str], debug, descriptions, dirname, names): ) win32verstamp.stamp(pathname, options) numStamped += 1 - except Exception as exc: + except OSError as exc: print( "Could not stamp", pathname, - "with", - options, + "Error", + exc.winerror, "-", - repr(exc), + exc.strerror, ) else: print("WARNING: description not provided for:", name) From bf9942324fb53828f1ed1ba49657de183ffdd0fa Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 1 Aug 2024 23:31:06 -0400 Subject: [PATCH 3/4] No more `global` in bulkstamp --- win32/scripts/VersionStamp/bulkstamp.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/win32/scripts/VersionStamp/bulkstamp.py b/win32/scripts/VersionStamp/bulkstamp.py index cd8619243..89be6347d 100644 --- a/win32/scripts/VersionStamp/bulkstamp.py +++ b/win32/scripts/VersionStamp/bulkstamp.py @@ -43,8 +43,6 @@ sys.path.append(os.path.abspath(__file__ + "/../../../Lib")) import win32verstamp -numStamped = 0 - g_patterns = [ "*.dll", "*.pyd", @@ -53,8 +51,9 @@ ] -def walk(vars: Mapping[str, str], debug, descriptions, dirname, names): - global numStamped +def walk(vars: Mapping[str, str], debug, descriptions, dirname, names) -> int: + """Returns the number of stamped files.""" + numStamped = 0 for name in names: for pat in g_patterns: if fnmatch.fnmatch(name, pat): @@ -84,6 +83,7 @@ def walk(vars: Mapping[str, str], debug, descriptions, dirname, names): else: print("WARNING: description not provided for:", name) # skip branding this - assume already branded or handled elsewhere + return numStamped # print("Stamped", pathname) @@ -127,9 +127,6 @@ def load_descriptions(fname, vars): def scan(build, root: str, desc, **custom_vars): - print(build, root, desc) - global numStamped - numStamped = 0 try: build = int(build) except ValueError: @@ -144,10 +141,11 @@ def scan(build, root: str, desc, **custom_vars): vars["build"] = build vars.update(custom_vars) + numStamped = 0 for directory, dirnames, filenames in os.walk(root): - walk(vars, debug, descriptions, directory, filenames) + numStamped += walk(vars, debug, descriptions, directory, filenames) - print("Stamped %d files." % (numStamped)) + print(f"Stamped {numStamped} files.") if __name__ == "__main__": From ee6f93026b19a6983ca3f59da6fdaf5fb7477940 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 1 Aug 2024 23:37:39 -0400 Subject: [PATCH 4/4] mypy check --- win32/Lib/_win32verstamp_pywin32ctypes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/win32/Lib/_win32verstamp_pywin32ctypes.py b/win32/Lib/_win32verstamp_pywin32ctypes.py index 8b151d94c..b878809b7 100644 --- a/win32/Lib/_win32verstamp_pywin32ctypes.py +++ b/win32/Lib/_win32verstamp_pywin32ctypes.py @@ -74,18 +74,18 @@ def check_false(result: int | None, function: _NamedFuncPointer, *_) -> Literal[ _BeginUpdateResource = kernel32.BeginUpdateResourceW _BeginUpdateResource.argtypes = [LPCWSTR, BOOL] _BeginUpdateResource.restype = HANDLE -_BeginUpdateResource.errcheck = check_null +_BeginUpdateResource.errcheck = check_null # type: ignore[assignment] # ctypes is badly typed _EndUpdateResource = kernel32.EndUpdateResourceW _EndUpdateResource.argtypes = [HANDLE, BOOL] _EndUpdateResource.restype = BOOL -_EndUpdateResource.errcheck = check_false +_EndUpdateResource.errcheck = check_false # type: ignore[assignment] # ctypes is badly typed _UpdateResource = kernel32.UpdateResourceW _UpdateResource.argtypes = [HANDLE, LPCWSTR, LPCWSTR, WORD, LPVOID, DWORD] _UpdateResource.restype = BOOL -_UpdateResource.errcheck = check_false +_UpdateResource.errcheck = check_false # type: ignore[assignment] # ctypes is badly typed ###