diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index 8452294..5b08776 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -28,7 +28,8 @@ requirements: - numpy==1.20.* # [py==37] - tomli>=2.0.1 - wheel>=0.36 - - sysroot_linux-64 # [linux64] + - sysroot_linux-64 2.17 # [linux64] + - kernel-headers_linux-64 3.10.0 # [linux64] run: - python diff --git a/docs/beta-features/index.md b/docs/beta-features/index.md index 83e211b..f150488 100644 --- a/docs/beta-features/index.md +++ b/docs/beta-features/index.md @@ -11,7 +11,7 @@ _This page provides an overview of PyKX Beta Features, including what they are, ## What is a Beta Feature? -As used commonly within software development "Beta Features" within PyKX describe features which have completed an initial development process phase and are being released in an opt-in manner to users of PyKX wishing to test these features. These features are not intended to be for production use while in beta and are subject to change prior to release as full features. Usage of these features will not effect the default behaviour of the library outside of the scope of the new functionality being added. +As used commonly within software development "Beta Features" within PyKX describe features which have completed an initial development process phase and are being released in an opt-in manner to users of PyKX wishing to test these features. These features are not intended to be for production use while in beta and are subject to change prior to release as full features. Usage of these features will not effect the default behavior of the library outside of the scope of the new functionality being added. Feedback on Beta Feature development is incredibly helpful and helps to determine when these features are promoted to fully supported production features. If you run into any issues while making use of these features please raise an issue on the PyKX Github [here](https://github.com/KxSystems/pykx/issues). diff --git a/docs/getting-started/installing.md b/docs/getting-started/installing.md index 292e5fa..b394b00 100644 --- a/docs/getting-started/installing.md +++ b/docs/getting-started/installing.md @@ -331,11 +331,11 @@ This command should display the installed version of PyKX. | Mac ARM | kdb+ 4.0 | libq.dylib | 2025.02.18 | | Mac x86 | kdb+ 4.0 | libq.dylib | 2025.02.18 | | Windows | kdb+ 4.0 | q.dll/q.lib | 2025.02.18 | -| Linux ARM | kdb+ 4.1 | libq.so | 2025.04.28 | -| Linux x86 | kdb+ 4.1 | libq.so | 2025.04.28 | -| Mac ARM | kdb+ 4.1 | libq.dylib | 2025.04.28 | -| Mac x86 | kdb+ 4.1 | libq.dylib | 2025.04.28 | -| Windows | kdb+ 4.1 | q.dll/q.lib | 2025.04.28 | +| Linux ARM | kdb+ 4.1 | libq.so | 2025.11.25 | +| Linux x86 | kdb+ 4.1 | libq.so | 2025.11.25 | +| Mac ARM | kdb+ 4.1 | libq.dylib | 2025.11.25 | +| Mac x86 | kdb+ 4.1 | libq.dylib | 2025.11.25 | +| Windows | kdb+ 4.1 | q.dll/q.lib | 2025.11.25 | | Linux ARM | Unlicensed | libe.so | 2023.11.22 | | Linux x86 | Unlicensed | libe.so | 2023.11.22 | | Mac ARM | Unlicensed | libe.so | 2023.11.22 | diff --git a/docs/index.md b/docs/index.md index 9f86e17..4fbec86 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,7 +34,7 @@ To begin your journey with PyKX, follow the sections below: !!! home-page "[Releases](./release-notes/changelog.md)" - Stay updated with the latest release notes and roadmap details. You’ll find information on the latest releases/fixes, previous versions, and upcoming features. + Stay updated with the latest release notes. You’ll find information on the latest releases/fixes, and previous versions. !!! home-page "[Help and Support](./help/troubleshooting.md)" diff --git a/docs/release-notes/changelog.md b/docs/release-notes/changelog.md index 16acbe6..199dad9 100644 --- a/docs/release-notes/changelog.md +++ b/docs/release-notes/changelog.md @@ -4,6 +4,49 @@ The changelog presented here outlines changes to PyKX when operating within a Python environment specifically, if you require changelogs associated with PyKX operating under a q environment see [here](./underq-changelog.md). +## PyKX 3.1.6 + +#### Release Date + +2025-12-01 + +### Fixes and Improvements + +- Updated 4.1 to 2025.11.25 for all platforms. +- Fixed issue where setting `sort=True` when calling `merge` on two `kx.Tables` ignored the supplied `on` parameter. + + === "Behavior prior to change" + + ```Python + >>> da = kx.q.z.D + >>> a = kx.toq(pd.DataFrame({'r':[2,3,4],'date':[da-2, da, da-1], 'k':[10, 11, 12]})) + >>> b = kx.toq(pd.DataFrame({'r':[5,6,7],'date':[da-2, da-1, da], 'k':[13, 14, 15]})) + >>> a.merge(b, on='date', sort=True) + pykx.Table(pykx.q(' + r_x date k_x r_y k_y + -------------------------- + 2 2025.11.25 10 5 13 + 3 2025.11.27 11 7 15 + 4 2025.11.26 12 6 14 + ')) + ``` + + === "Behavior post change" + + ```Python + >>> da = kx.q.z.D + >>> a = kx.toq(pd.DataFrame({'r':[2,3,4],'date':[da-2, da, da-1], 'k':[10, 11, 12]})) + >>> b = kx.toq(pd.DataFrame({'r':[5,6,7],'date':[da-2, da-1, da], 'k':[13, 14, 15]})) + >>> a.merge(b, on='date', sort=True) + pykx.Table(pykx.q(' + r_x date k_x r_y k_y + -------------------------- + 2 2025.11.25 10 5 13 + 4 2025.11.26 12 6 14 + 3 2025.11.27 11 7 15 + ')) + ``` + ## PyKX 3.1.5 #### Release Date @@ -30,24 +73,24 @@ - Added `no_allocator` keyword argument to `pykx.toq` that allows one time disabling of the PyKX allocator during a conversion. See [here](../help/issues.md#known-issues) for details. - Fixed an issue when converting dataframes with embeddings arrays. - === "Behaviour prior to change" + === "Behavior prior to change" ```Python >>> df=pd.DataFrame(dict(embeddings=list(np.random.ranf((500, 10)).astype(np.float32)))) - >>> pykx.toq(df) + >>> kx.toq(df) segfault ``` - === "Behaviour post change" + === "Behavior post change" ```Python >>> df=pd.DataFrame(dict(embeddings=list(np.random.ranf((500, 10)).astype(np.float32)))) - >>> pykx.toq(df) + >>> kx.toq(df) ``` -- Addition of `__array__` method to Atom classes. Enables `np.asarray` to created typed arrays. +- Addition of `__array__` method to Atom classes. Enables `np.asarray` to create typed arrays. - === "Behaviour prior to change" + === "Behavior prior to change" ```python >>> np.asarray(kx.FloatAtom(3.65)).dtype @@ -58,7 +101,7 @@ dtype('O') ``` - === "Behaviour post change" + === "Behavior post change" ```python >>> np.asarray(kx.FloatAtom(3.65)).dtype @@ -71,7 +114,7 @@ - Fixed the returned type of an `exec` query with a single renamed column. - === "Behaviour prior to change" + === "Behavior prior to change" ```python >>> type(kx.q.qsql.exec(qtab, {'symcol': 'col1'})) @@ -80,7 +123,7 @@ pykx.wrappers.SymbolVector ``` - === "Behaviour after change" + === "Behavior after change" ```python >>> type(kx.q.qsql.exec(qtab, {'symcol': 'col1'})) @@ -89,6 +132,10 @@ pykx.wrappers.Dictionary ``` +### Deprecations & Removals + +- Creating more than one `DB` instance without specifying `overwrite=True` + ## PyKX 3.1.4 #### Release Date @@ -379,8 +426,8 @@ │ └── time ``` -- Fixed behaviour for `PartitionedTable.copy_column()` operation on anymap columns. When copying `anymap` columns, the `#` and `##` files were not copied. Now all correct copying procedures are applied. -- Fixed behaviour for `PartitionedTable.delete_column()` operation on anymap columns. When deleting `anymap` columns, the `#` and `##` files were left in. All relevant files are now deleted. +- Fixed behavior for `PartitionedTable.copy_column()` operation on anymap columns. When copying `anymap` columns, the `#` and `##` files were not copied. Now all correct copying procedures are applied. +- Fixed behavior for `PartitionedTable.delete_column()` operation on anymap columns. When deleting `anymap` columns, the `#` and `##` files were left in. All relevant files are now deleted. - Fix creation of `ParseTree` objects from `QueryPhrase` or `Column` objects. - Fix or operator `|` for `Column | ParseTree` use cases. - Fixed an issue around the installation process when users attempted to set unlicensed mode after PyKX failed to load with a kdb+ license. diff --git a/docs/release-notes/deprecations.md b/docs/release-notes/deprecations.md index bdc7fac..268120d 100644 --- a/docs/release-notes/deprecations.md +++ b/docs/release-notes/deprecations.md @@ -2,8 +2,9 @@ A list of deprecated behaviors and the version in which they were removed. -| Feature | Alternative | Deprecated | Removed | +| Feature | Alternative | Deprecated | Removed | |---------------------------------------------------|---------------------------|---------------|------------| +| Creating more than one DB object | `overwrite=True` | 3.1.5 | | | `kx.q.system.console_size` | `kx.q.system.display_size`| 3.1.3 | | | `.pykx.console[]` on Windows | | 3.1.3 | 3.1.3 | | `labels` keyword for `rename` method | `mapper` | 2.5.0 | 3.1.0 | @@ -20,6 +21,22 @@ A list of deprecated behaviors and the version in which they were removed. | `PYKX_ENABLE_PANDAS_API` | | 3.0.0 | 3.0.0 | | `.pd(raw_guids)` | | 2.5.0 | 2.5.0 | +## PyKX 3.1.5 + +Release Date: 2025-10-21 + +### Fixes and Improvements + +- Users will be warned when they attempt to create more than one `DB` object. + + ```python + >>> import pykx as kx + >>> db = kx.DB(path="tmp/db1") + >>> db2 = kx.DB(path="tmp/db2") + PyKXWarning: Only one DB object exists at a time within a process. Use overwrite=True to overwrite your existing DB object. This warning will error in future releases. + >>> db3 = kx.DB(path="tmp/db3", overwrite=True) + >>> + ``` ## PyKX 3.1.3 diff --git a/docs/release-notes/underq-changelog.md b/docs/release-notes/underq-changelog.md index c9e8bc1..e5b3e51 100644 --- a/docs/release-notes/underq-changelog.md +++ b/docs/release-notes/underq-changelog.md @@ -26,7 +26,7 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat - Resolved `object has no attribute 't'` error for certain conversions - === "Behaviour prior to change" + === "Behavior prior to change" ```python q).pykx.setdefault (),"k"; @@ -39,7 +39,7 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat ":"~first a0:string x0; ``` - === "Behaviour post change" + === "Behavior post change" ```python q).pykx.setdefault (),"k"; @@ -137,14 +137,14 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat - Using `.pykx.toq`/`.pykx.toq0` now return the q representation of an object when passed a wrapped type conversion object - === "Behaviour prior to change" + === "Behavior prior to change" ```q q).pykx.toq .pykx.topd ([] a:1 2 3) enlist[`..pandas;;][... ``` - === "Behaviour post change" + === "Behavior post change" ```q q).pykx.toq .pykx.topd[([] a:1 2 3)] @@ -157,14 +157,14 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat - When using `.pykx.toq`/`.pykx.toq0`, passing compositions such as `any` now returns the data as the appropriate object - === "Behaviour prior to change" + === "Behavior prior to change" ```q q).pykx.toq any 'Expected foreign object for call to .pykx.toq ``` - === "Behaviour post change" + === "Behavior post change" ```q q).pykx.toq any @@ -173,14 +173,14 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat - When failing to find a file loaded with `.pykx.loadPy` the name of the file which was loaded is now included in the error message - === "Behaviour prior to change" + === "Behavior prior to change" ```q q).pykx.loadPy "file.py" 'FileNotFoundError(2, 'No such file or directory') ``` - === "Behaviour post change" + === "Behavior post change" ```q q).pykx.loadPy "file.py" @@ -233,7 +233,7 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat ### Fixes and Improvements -- Addition of function `.pykx.toq0` to support conversion of Python strings to q strings rather than q symbols as is the default behaviour +- Addition of function `.pykx.toq0` to support conversion of Python strings to q strings rather than q symbols as is the default behavior ```q q)pystr:.pykx.eval["\"test\""] @@ -245,7 +245,7 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat - Fix for `install_into_QHOME` with `overwrite_embedpy=True`. Previously loading PyKX through use of `p)` would fail. - === "Behaviour prior to change" + === "Behavior prior to change" ```q q)p)print(1+1) @@ -253,14 +253,14 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat [3] /home/user/q/p.k:1: \l pykx.q ``` - === "Behaviour post change" + === "Behavior post change" ```q q)p)print(1+1) 2 ``` -- Fix to minor memory leak when accessing attributes or retrieving global variables from Python objects. The following operations would lead to this behaviour +- Fix to minor memory leak when accessing attributes or retrieving global variables from Python objects. The following operations would lead to this behavior ```q q)np:.pykx.import[`numpy] @@ -273,7 +273,7 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat - When loading on Linux loading of `qlog` no longer loads the logging functionality into the `.pykx` namespace and instead loads it to the `.com_kx_log` namespace as expected under default conditions. - === "Behaviour prior to change" + === "Behavior prior to change" ```q q)@[{get x;1b};`.pykx.configure;0b] @@ -282,7 +282,7 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat 0b ``` - === "Behaviour post change" + === "Behavior post change" ```q q)@[{get x;1b};`.pykx.configure;0b] @@ -311,7 +311,7 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat - Previously PyKX conversions of generic lists (type 0h) would convert this data to it's `raw` representation rather than it's `python` representation as documented. This had the effect of restricting the usability of some types within PyKX under q in non-trivial use-cases. With the `2.5.2` changes to more accurately represent `raw` data at depth this became more obvious as an issue. - === "Behaviour prior to change" + === "Behavior prior to change" ```q q).pykx.version[] @@ -320,7 +320,7 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat [b'test', None, 49577290277400616] ``` - === "Behaviour post change" + === "Behavior post change" ```q q).pykx.print .pykx.eval["lambda x:x"] @@ -586,7 +586,7 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat - Update to default conversion logic for q objects passed to PyKX functions to more closely match embedPy based conversion expectations.For version <=2.0 conversions of KX lists would produce N Dimensional Numpy arrays of singular type. This results in issues when applying to many analytic libraries which rely on lists of lists rather than singular N Dimensional arrays. Additionally q tables and keyed tables would be converted to Numpy recarrays, these are now converted to Pandas DataFrames. To maintain previous behavior please set the following environment variable `PYKX_DEFAULT_CONVERSION="np"`. - === "Behaviour prior to change" + === "Behavior prior to change" ```q q).pykx.eval["lambda x:print(type(x))"](10?1f;10?1f) @@ -595,7 +595,7 @@ This changelog provides updates from PyKX 2.0.0 and above, for information relat ``` - === "Behaviour post change" + === "Behavior post change" ```q q).pykx.eval["lambda x:print(type(x))"](10?1f;10?1f) diff --git a/docs/upgrades/2030.md b/docs/upgrades/2030.md index 9edd74d..d6bf898 100644 --- a/docs/upgrades/2030.md +++ b/docs/upgrades/2030.md @@ -17,13 +17,13 @@ _This page outlines key differences when upgrading PyKX versions from 2.5.* to 3 - Additional required dependencies for this feature are now part of the required dependencies. - === "Previous behaviour" + === "Previous behavior" ```bash pip install pykx[beta] ``` - === "New behaviour" + === "New behavior" ```bash pip install pykx @@ -36,7 +36,7 @@ _This page outlines key differences when upgrading PyKX versions from 2.5.* to 3 This changed to a single function call. - === "Previous behaviour" + === "Previous behavior" ```python >>> import pykx as kx @@ -44,7 +44,7 @@ _This page outlines key differences when upgrading PyKX versions from 2.5.* to 3 >>> session.create(host='localhost', port=5050) ``` - === "New behaviour" + === "New behavior" ```python >>> import pykx as kx @@ -56,7 +56,7 @@ _This page outlines key differences when upgrading PyKX versions from 2.5.* to 3 - Previously this was done using a function call to `#!python session.add_library`. This function would specify the libraries to be loaded on first execution of the function and expected the names of the libraries to be loaded as a list of arguments. - Now you can use the keyword `#!python libraries` at session creation to load the libraries. Also, the library addition function is now called `session.libraries` to match the API for streaming with PyKX. Finally the `#!python libraries` keyword and function take a dictionary mapping the aliased name for the library to the library which is to be imported, namely `#!python import numpy as np` would be defined as `#!python {'np': 'numpy'}`. - === "Previous Behaviour" + === "Previous Behavior" ```python >>> import pykx as kx @@ -65,7 +65,7 @@ _This page outlines key differences when upgrading PyKX versions from 2.5.* to 3 >>> session.add_library('numpy', 'pykx') ``` - === "New Behaviour" + === "New Behavior" ```python >>> import pykx as kx @@ -79,7 +79,7 @@ _This page outlines key differences when upgrading PyKX versions from 2.5.* to 3 - The `#!python clear` method provided for `#!python session` objects is now called `#!python close`. This change aligns the naming with IPC communication channels being 'closed' when stopping communication with a remote session and aligns with the naming used within the IPC module - === "Previous Behaviour" + === "Previous Behavior" ```python >>> import pykx as kx @@ -88,7 +88,7 @@ _This page outlines key differences when upgrading PyKX versions from 2.5.* to 3 >>> session.clear() ``` - === "New Behaviour" + === "New Behavior" ```python >>> import pykx as kx @@ -112,7 +112,7 @@ _This page outlines key differences when upgrading PyKX versions from 2.5.* to 3 | `PYKX_ENABLE_PANDAS_API` | No longer applicable | - Removal of the now deprecated `#!python modify` keyword for `#!python select`, `#!python exec`, `#!python update` and `#!python delete` operations on `#!python pykx.Table` and `#!python pykx.KeyedTable`. This has been permanently changed to be use `#!python inplace`. -- Removal of the deprecated `#!python replace_self` keyword when attempting to overwrite a `#!python pykx.Table` or `#!python KeyedTable` using insert/upsert functionality. To maintain this behaviour use the `#python inplace` keyword. +- Removal of the deprecated `#!python replace_self` keyword when attempting to overwrite a `#!python pykx.Table` or `#!python KeyedTable` using insert/upsert functionality. To maintain this behavior use the `#python inplace` keyword. ## Error message changes @@ -133,9 +133,9 @@ Various `#!python pykx.QError` error messages now provide more verbose explanati ## Null and Infinite conversion changes PyKX previously left some null and infinite values unconverted, now these are converted to native Python objects. -The behaviour of Atom and Vector conversions has also been updated to more closely match each other. +The behavior of Atom and Vector conversions has also been updated to more closely match each other. -The links below outline the full before and after behaviour. +The links below outline the full before and after behavior. - [Null Conversions](../user-guide/fundamentals/nulls_and_infinities.md#null-conversions). - [Infinite Conversions](../user-guide/fundamentals/nulls_and_infinities.md#infinite-conversions). diff --git a/docs/user-guide/advanced/Pandas_API.ipynb b/docs/user-guide/advanced/Pandas_API.ipynb index 0622e7a..7cb4833 100644 --- a/docs/user-guide/advanced/Pandas_API.ipynb +++ b/docs/user-guide/advanced/Pandas_API.ipynb @@ -2072,7 +2072,7 @@ "id": "caa8cb07", "metadata": {}, "source": [ - "Merge tab1 and tab2_keyed using a left join with `q_join` set to `True`. Inputs/Outputs will match q [lj](https://code.kx.com/q/ref/lj/) behaviour." + "Merge tab1 and tab2_keyed using a left join with `q_join` set to `True`. Inputs/Outputs will match q [lj](https://code.kx.com/q/ref/lj/) behavior." ] }, { @@ -2093,7 +2093,7 @@ "id": "b465b9fc", "metadata": {}, "source": [ - "Inputs/Outputs will match q [ij](https://code.kx.com/q/ref/ij/) behaviour." + "Inputs/Outputs will match q [ij](https://code.kx.com/q/ref/ij/) behavior." ] }, { diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index 5cee4dc..567d6cd 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -115,7 +115,7 @@ To enable or disable advanced features of PyKX across all modes of operation, us | `PYKX_4_1_ENABLED` | `False` | `1` or `true` | Load version 4.1 of `libq` when starting `PyKX` in licensed mode, this environment variable does not work without a valid `q` license. | | `PYKX_JUPYTERQ` | `False` | `1` or `true` | When enabled, any Jupyter Notebook will start in q first mode by default when PyKX is imported. | | `PYKX_Q_EXECUTABLE` | `q` | string denoting path to q executable | This allows users to specify the location of the q executable which should be called when using making use of the `tick` module for defining streaming infrastructures | -| `PYKX_SUPPRESS_WARNINGS` | `False` | `1` or `true` | This allows the user to suppress warnings that have been suggested as sensible to be raised by users for PyKX in situations where edge cases can result in unexpected behaviour. Warnings in scenarios where a decision has been made to not support behaviour explicitly rather than where user discretion is required are still maintained. | +| `PYKX_SUPPRESS_WARNINGS` | `False` | `1` or `true` | This allows the user to suppress warnings that have been suggested as sensible to be raised by users for PyKX in situations where edge cases can result in unexpected behavior. Warnings in scenarios where a decision has been made to not support behavior explicitly rather than where user discretion is required are still maintained. | | `PYKX_CONFIGURATION_LOCATION` | `.` | The path to the folder containing the `.pykx-config` file. | This allows users to specify a location other than the `.` or a users `home` directory to store their configuration file outlined [here](#configuration-file) | | `PYKX_CONFIGURATION_PROFILE` | `default` | The "profile" defined in `.pykx-config` file to be used. | Users can specify which set of configuration variables are to be used by modifying the `PYKX_CONFIGURATION_PROFILE` variable see [here](#configuration-file) for more details. Note that this configuration can only be used as an environment variable. | diff --git a/docs/user-guide/fundamentals/conversion_considerations.md b/docs/user-guide/fundamentals/conversion_considerations.md index 20213ee..efe081e 100644 --- a/docs/user-guide/fundamentals/conversion_considerations.md +++ b/docs/user-guide/fundamentals/conversion_considerations.md @@ -28,7 +28,7 @@ Handling and converting [text in PyKX](./text.md) requires consideration as ther ## Nulls and Infinites -Most q datatypes have the concepts of null, negative infinity, and infinity. Python does not have the concept of infinites and it's null behaviour differs in implementation. The page [handling nulls and infinities](./nulls_and_infinities.md) details the needed considerations when dealing with these special values. +Most q datatypes have the concepts of null, negative infinity, and infinity. Python does not have the concept of infinites and it's null behavior differs in implementation. The page [handling nulls and infinities](./nulls_and_infinities.md) details the needed considerations when dealing with these special values. ## Temporal data types diff --git a/docs/user-guide/fundamentals/nulls_and_infinities.md b/docs/user-guide/fundamentals/nulls_and_infinities.md index 7924ec7..e00da72 100644 --- a/docs/user-guide/fundamentals/nulls_and_infinities.md +++ b/docs/user-guide/fundamentals/nulls_and_infinities.md @@ -138,7 +138,7 @@ See also the page with specifics on [temporal](./temporal.md) conversions. !!! note "Note" - PyKX null conversion behaviour changed in version 3.0.0. The below table outlines the before and after conversions. + PyKX null conversion behavior changed in version 3.0.0. The below table outlines the before and after conversions. === ".py()" @@ -307,7 +307,7 @@ x -- 2 3 ``` -However, when displaying with multi-index columns, the mask behaviour is not adhered to: +However, when displaying with multi-index columns, the mask behavior is not adhered to: ```python >>> keytab = kx.q.xkey(['x', 'x1'], @@ -417,7 +417,7 @@ See also the page with specifics on [temporal](./temporal.md) conversions to exp !!! note "Note" - PyKX infinite conversion behaviour changed in version 3.0.0. The below tables outline the before and after conversions. + PyKX infinite conversion behavior changed in version 3.0.0. The below tables outline the before and after conversions. #### Positive Infinity conversions diff --git a/mkdocs.yml b/mkdocs.yml index ee48703..ab8cdf6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -290,7 +290,6 @@ nav: - PyKX under q: release-notes/underq-changelog.md - Deprecations: release-notes/deprecations.md - 2.x -> 3.x Upgrade : upgrades/2030.md - - Roadmap: roadmap.md - Beta features: - Introduction: beta-features/index.md - PyTorch Conversions: beta-features/torch.md diff --git a/setup.py b/setup.py index a620164..ffb1742 100755 --- a/setup.py +++ b/setup.py @@ -138,6 +138,7 @@ def build_q_c_extensions(self): self.build_q_c_extension(compiler, 'pykxq', lib_ext) if system != 'Windows': self.build_q_c_extension(compiler, '_tcore', lib_ext, library=['pthread']) + self.build_q_c_extension(compiler, 'pykxq_m', lib_ext, library=['pthread']) def cythonize_extensions(extensions: List[Extension]) -> List[Extension]: diff --git a/src/pykx/.gitignore b/src/pykx/.gitignore index 8c93224..26101ad 100644 --- a/src/pykx/.gitignore +++ b/src/pykx/.gitignore @@ -5,6 +5,7 @@ !python.c !pykx.c !pykxq.c +!pykxq_m.c !numpy_conversions.c !_tcore.c *.html diff --git a/src/pykx/core.pyx b/src/pykx/core.pyx index a35d287..cfede9e 100644 --- a/src/pykx/core.pyx +++ b/src/pykx/core.pyx @@ -102,7 +102,7 @@ final_qhome = str(qhome if ignore_qhome else pykx_lib_dir) if '--licensed' in qargs and '--unlicensed' in qargs: raise PyKXException("$QARGS includes mutually exclusive flags '--licensed' and '--unlicensed'") elif _pykx_force_unlicensed and _pykx_force_licensed: - raise PyKXException("User specified options for setting 'licensed' and 'unlicensed' behaviour " + raise PyKXException("User specified options for setting 'licensed' and 'unlicensed' behavior " "resulting in conflicts") class QLock: diff --git a/src/pykx/embedded_q.py b/src/pykx/embedded_q.py index 0379adf..a1ce504 100644 --- a/src/pykx/embedded_q.py +++ b/src/pykx/embedded_q.py @@ -151,8 +151,11 @@ def __init__(self): # noqa code += f'2:[`$"{pykx_qlib_path}";(`k_pykx_init; 2)][`$"{find_core_lib("q").as_posix()}";{"1b" if pykx_threading else "0b"}];' # noqa: E501 code += f'`.pykx.modpow set {{((`$"{pykx_qlib_path}") 2: (`k_modpow; 3))["j"$x;"j"$y;$[z~(::);(::);"j"$z]]}};' # noqa: E501 else: - code += f'2:[`$"{pykx_qlib_path}q";(`k_pykx_init; 2)][`$"{find_core_lib("q").as_posix()}";{"1b" if pykx_threading else "0b"}];' # noqa: E501 - code += f'`.pykx.modpow set {{((`$"{pykx_qlib_path}q") 2: (`k_modpow; 3))["j"$x;"j"$y;$[z~(::);(::);"j"$z]]}};' # noqa: E501 + suffix = "" + if os.getenv("PYKXQ_THREADING") is not None: + suffix = "_m" + code += f'2:[`$"{pykx_qlib_path}q{suffix}";(`k_pykx_init; 2)][`$"{find_core_lib("q").as_posix()}";{"1b" if pykx_threading else "0b"}];' # noqa: E501 + code += f'`.pykx.modpow set {{((`$"{pykx_qlib_path}q{suffix}") 2: (`k_modpow; 3))["j"$x;"j"$y;$[z~(::);(::);"j"$z]]}};' # noqa: E501 code += '@[get;`.pykx.i.kxic.loadfailed;{()!()}]' kxic_loadfailed = self._call(code, skip_debug=True).py() if (platform.system() != "Linux") and (not no_qce) and ('--no-sql' not in qargs): diff --git a/src/pykx/ipc.py b/src/pykx/ipc.py index 7091f6e..fb60e69 100644 --- a/src/pykx/ipc.py +++ b/src/pykx/ipc.py @@ -2108,7 +2108,7 @@ def __init__(self, Note: When querying KX Insights the `#!python no_ctx=True` keyword argument must be used. Note: 3.1 Upgrade considerations - As of PyKX version 3.1 all QFuture objects returned from calls to `RawQConnection` + As of `pykx` version 3.1 all QFuture objects returned from calls to `RawQConnection` objects must be awaited to receive their results. Previously you could use just `conn.poll_recv()` and then directly get the result with `future.result()`. diff --git a/src/pykx/lib/4-1-libs/q.k b/src/pykx/lib/4-1-libs/q.k index 66528b8..68d1d3e 100644 --- a/src/pykx/lib/4-1-libs/q.k +++ b/src/pykx/lib/4-1-libs/q.k @@ -95,7 +95,7 @@ unm:{$[x~?x;x;@[x;j;:;(`$($x),'$i)j:&0@x;`${$[(*x)in n,"_ ";"a",x;x]}x@&(x:$x)in an;ft[{$[98h=@x;+.z.s[+x];[s[i]:`$($s i:&((s:id'!x) in`i,res,!`.q)),'"1";({unm[x]}/s)!. x]]}]x]} j10:64/:b6?;x10:b6@0x40\: /base64 J from char10 j12:36/:nA?;x12:nA@0x24\: /base36 J from char12(cusip) -btoa:-32!;sha1:-33!;prf0:+`name`file`line`col`text`pos!*-37! +btoa:-32!;atob:-44!;sha1:-33!;prf0:+`name`file`line`col`text`pos!*-37! objp:{x like"[mgs][s3]://*"}; lo:{if[$[1>@d:!f:-1!x;1;`.d~*d];:.[$[qt d;*|`\:f;99=@d;`.;'`type];();:;d:. f]];d@:&~d like"*$";p:(d=`par.txt)|d like"[0-9]*"; if[y;if[objp x;'"no cd to object storage"];."\\cd ",$x;f:`:.];{.q.set[*|`\:x]$[0h>@!x;. x;x`]}'`/:'f,'d@&~(d=`html)|(d like"*#")|p|s:d like"*.?";if[|/p;L[d@&p;f;~y|objp x]];if[z&~`.=x;(."\\l ",$:)'`/:'f,'d@&s&~p];} @@ -121,7 +121,7 @@ bvfp:{g:$[(::)~x;max;min];x:.Q.d;d:{$[y~pv;`/:'x,'d@&(d:!x)in`$$y;u@&{11h=@!x}'u t:?,/!:'vt:{(&#:'x)(=,/. x)}'{({("DMJJ"`date`month`year`int?pf)$$last@`\:x}'x)!!:':x}'d; if[#nt:(exc:.q.except)[t;!vp];d:{`/:'x[(. y)[;0]],'(`$$(. y)[;1]),'!y}[P]@{i:y@&:x=y x:@[x;&x~\:();:;*0#pv];(i;x i)}[;g]'+:nt#/:g''nt#/:vt;.Q.vp,:nt!{(+(,pf)!,0#pv),'+(-2!'.+x)#'+|0#x:?[x;();0b;()]}':d;.Q.vt:.Q.vt,\:nt!(#nt)#,exc[?pv;y]];.Q.vt:(.Q.vt,''P!exc[y]''vt)exc''vt:(nt,!vp)#/:vt;{.[x;();:;+.q.except[!+vp x;pf]!x]}'exc[!vp;pt];pt::.q.asc@!vp;vpv::?pv;} bvi:{$[(`vpv in !.Q);bvfp[x;?.q.except[pv;vpv]];bv x]} -bv:{vt::(,`)!,()!();vp::(0#`)!();vpv::0#pv;bvfp[x;pv]} +bv:{vt::(,`)!,()!();vp::(0#`)!();vpv::0#pv;bvfp[x;?pv]} sp:{$[0>."\\s";x'y;x':y]} pt:pm:();MAP:{{$[0>@a:.+0!. x;.q.set[x]@.`$-1_$a;]}'a@&~(a:."\\a")in pt;pm::();if[#pt;pm::pt!sp[{(`u#pd,'pv)!sp[p2[(x;();0b;())]/;+(pd;pv)]}]pt]} @@ -224,7 +224,7 @@ sc:"$-.+!*'(),",.Q.an;hug:{,/@["%",'$"x"$!256;x;:;x]@};hu:hug sc /rfc1738 safe ty:{(`$*x)!*|x:+" "\:'x}("htm text/html";"html text/html";"csv text/comma-separated-values";"txt text/plain";"xml text/plain";"xls application/msexcel" "gif image/gif";"jpg image/jpeg";"png image/png";"bmp image/bmp";"ico image/x-icon";"svg image/svg+xml";"pdf application/pdf";"css text/css" "zip application/x-compressed";"js application/x-javascript";"doc application/msword";"swf application/x-shockwave-flash";"json application/json") -hnz:{[a;b;c;d]"HTTP/1.1 ",a,$[b;"\r\nContent-Encoding: gzip";""],"\r\nContent-Type: ",$[#t:ty c;t;"application/octet-stream"],"\r\nConnection: close\r\nContent-Length: ",($#d),"\r\n\r\n",d:$[b:(-35!)[]&b&2000<#d;-35!(6;d);d]} +hnz:{[a;b;c;d]"HTTP/1.1 ",a,$[b;"\r\nContent-Encoding: gzip";""],"\r\nContent-Type: ",$[#t:ty c;t;"application/octet-stream"],"\r\nConnection: close\r\nContent-Length: ",($#d),"\r\n\r\n",d:10h$$[b:(-35!)[]&b&2000<#d;-35!(6;d);d]} hn:hnz[;0];HOME:"html";hy:hn"200 OK";hp:hy[`htm]html pre@;he:hn["400 Bad Request";`txt]"'", /GET /[r.{txt|csv|xml|xsl}]?.. or ?[J val:.:; .z.ph:{gz:#ss[x[1]`$"Accept-Encoding";"gzip"];x:uh$[@x;x;*x];tf:$[2=."\\e";{.Q.trp[y;z;{he y,"\n",.Q.sbt@(-x)_z}x]};{x;@[y;z;he]}];$[~#x;hy[`htm]fram[$.z.f;x]("?";"?",*x@ +#include +#include +#include +#include "k.h" +#include "py.h" +#define Py_True _Py_TrueStruct +#define Py_False _Py_FalseStruct +#include + +void* q_lib; + +static P sys; +static P builtins; +static P toq_module; +static P toq; +static P wrappers_module; +static P py_wrappers_module; +static P exception_tracker; +static P factory; +static P pyfactory; +static P k_factory; +static P k_dict_converter; +static P thread_get_ident; +static P POSITIONAL_ONLY; +static P POSITIONAL_OR_KEYWORD; +static P VAR_POSITIONAL; +static P KEYWORD_ONLY; +static P VAR_KEYWORD; +static P error_preamble; + +static P M, errfmt; +static void** N; +static bool setup; +static bool import_setup; + +int pykx_flag = -1; + +// Equivalent to starting Python with the `-S` flag. Allows us to edit some global config variables +// before `site.main()` is called. +int Py_NoSiteFlag = 1; + +enum CFunc { + REPR, + GET_ATTR, + GET_GLOBAL, + SET_ATTR, + SET_GLOBAL, + K_PY_RUN, + K_PY_FOREIGN, + FOREIGN_TO_Q, + IMPORT, + CALL, +}; +struct CallArgs { + enum CFunc func; + bool done; + K arg1; + K arg2; + K arg3; + K arg4; + K result; +}; +struct CallNode { + struct CallArgs* call; + struct CallNode* next; +}; + +static bool kill_thread; +static struct CallNode* calls_head = NULL; +static struct CallNode* calls_tail = NULL; +static pthread_t thread; +static pthread_mutex_t head_mutex; +static pthread_mutex_t cond_mutex; +static pthread_cond_t cond; + +struct CallNode* create_node(enum CFunc function) { + struct CallArgs* args = (struct CallArgs*)malloc(sizeof(struct CallArgs)); + args->func = function; + args->done = false; + args->arg1 = NULL; + args->arg2 = NULL; + args->arg3 = NULL; + args->arg4 = NULL; + args->result = NULL; + struct CallNode* node = (struct CallNode*)malloc(sizeof(struct CallNode)); + node->call = args; + node->next = NULL; + return node; +} + +void delete_node(struct CallNode* node) { + node->call->arg1 = NULL; + node->call->arg2 = NULL; + node->call->arg3 = NULL; + node->call->arg4 = NULL; + node->call->result = NULL; + free(node->call); + node->call = NULL; + node->next = NULL; + free(node); +} + +static P get_py_ptr(K f) { + return (P)kK(f)[1]; +} + + +static void py_destructor(K x) { + //int g = PyGILState_Ensure(); + //Py_XDECREF((P)kK(x)[1]); + //PyGILState_Release(g); +} + +static char* zs(K x) { + char* s=memcpy(malloc(x->n+1),x->G0,x->n); + return s[x->n]=0,s; +} + +static int check_py_foreign(K x){return x->t==112 && x->n==2 && *kK(x)==(K)py_destructor;} + +EXPORT K k_check_python(K x){return kb(check_py_foreign(x));} + +EXPORT K k_pykx_init(K k_q_lib_pat, K _pykx_threading) { + PyGILState_STATE gstate; + gstate = PyGILState_Ensure(); + + sys = PyModule_GetDict(PyImport_ImportModule("sys")); + builtins = PyModule_GetDict(PyImport_ImportModule("builtins")); + toq_module = PyModule_GetDict(PyImport_AddModule("pykx.toq")); + toq = PyDict_GetItemString(toq_module, "toq"); + wrappers_module = PyModule_GetDict(PyImport_AddModule("pykx._wrappers")); + py_wrappers_module = PyModule_GetDict(PyImport_AddModule("pykx.wrappers")); + exception_tracker = PyDict_GetItemString(wrappers_module, "_current_exception"); + factory = PyDict_GetItemString(wrappers_module, "_factory"); + pyfactory = PyDict_GetItemString(wrappers_module, "_pyfactory"); + k_factory = PyDict_GetItemString(py_wrappers_module, "_internal_k_list_wrapper"); + k_dict_converter = PyDict_GetItemString(py_wrappers_module, "_internal_k_dict_to_py"); + thread_get_ident = PyDict_GetItemString(PyModule_GetDict(PyImport_AddModule("threading")), "get_ident"); + P Parameter = PyDict_GetItemString(PyModule_GetDict(PyImport_AddModule("inspect")), "Parameter"); + POSITIONAL_ONLY = PyObject_GetAttrString(Parameter, "POSITIONAL_ONLY"); + POSITIONAL_OR_KEYWORD = PyObject_GetAttrString(Parameter, "POSITIONAL_OR_KEYWORD"); + VAR_POSITIONAL = PyObject_GetAttrString(Parameter, "VAR_POSITIONAL"); + KEYWORD_ONLY = PyObject_GetAttrString(Parameter, "KEYWORD_ONLY"); + VAR_KEYWORD = PyObject_GetAttrString(Parameter, "VAR_KEYWORD"); + error_preamble = PyUnicode_FromString("Error in pykx q extension library: "); + + PyGILState_Release(gstate); + return (K)0; +} + +void* thread_init(); +EXPORT K k_init_python(K x, K y, K z) { + pthread_mutex_init(&head_mutex, NULL); + pthread_mutex_init(&cond_mutex, NULL); + pthread_cond_init(&cond, NULL); + kill_thread = false; + setup = false; + import_setup = false; + /* + struct CallArgs* init_args = (struct CallArgs*)_init_args; + K x = init_args->arg1; + K y = init_args->arg2; + K z = init_args->arg3; + */ + static int i=0; + int f,g; + char* l; + char* h; + char* hh; + K n,v; + P a,b,pyhome; + P(i,0)l=zs(x),h=zs(y),hh=zs(z); + f=pyl(l); + free(l); + P(!f,krr("libpython")) + if(!Py_IsInitialized()){ + Py_SetPythonHome(Py_DecodeLocale(h,0)); + Py_SetProgramName(Py_DecodeLocale(hh,0)); + Py_InitializeEx(0); + if(PyEval_ThreadsInitialized()&&!PyGILState_Check()) + PyEval_RestoreThread(PyGILState_GetThisThreadState()); + } + M = PyModule_GetDict(PyImport_AddModule("__main__")); + n = ktn(KS,0); + v = ktn(0,0); + if(a = PyImport_ImportModule("numpy.core.multiarray")){ + N = PyCapsule_GetPointer(b=PyObject_GetAttrString(a,"_ARRAY_API"),0); + if(!N||!pyn(N)) + N=0; + Py_DecRef(b); + Py_DecRef(a); + } + PyErr_Clear(); + if(a=PyImport_ImportModule("traceback")) { + errfmt=PyObject_GetAttrString(a,"format_exception"); + Py_DecRef(a); + } + PyErr_Clear(); + struct CallArgs* init_args = malloc(sizeof(struct CallArgs)); + init_args->arg1 = x; + init_args->arg2 = y; + init_args->arg3 = z; + pthread_create(&thread, NULL, thread_init, (void*)init_args); + return (K)0; +} + + +K raise_k_error(char* error_str) { + K z = ks(error_str); + z->t = -128; + return z; +} + + +static K create_foreign(P p) { + K x = knk(2, py_destructor, (uintptr_t)p); + x->t = 112; + Py_INCREF(p); + return x; +} + +void flush_stdout() { + P out = PyDict_GetItemString(sys, "stdout"); + if ( PyObject_HasAttrString(out, "flush") ) { + PyObject_CallMethod(out, "flush", NULL); + } +} + +K k_py_error() { + if (!PyErr_Occurred()) return (K)0; + P ex_type; + P ex_value; + P ex_traceback; + PyErr_Fetch(&ex_type, &ex_value, &ex_traceback); + PyErr_NormalizeException(&ex_type, &ex_value, &ex_traceback); + if (ex_traceback) PyException_SetTraceback(ex_value, ex_traceback); + + // Build a q error object with the repr of the exception value as its message. The full + // traceback is provided as the cause to the QError that will be raised. + P ex_repr = PyObject_CallMethod(ex_value, "__repr__", NULL); + K k = ks((char*)PyUnicode_AsUTF8(ex_repr)); + k->t = -128; + Py_XDECREF(ex_repr); + // Store the Python exception in `k._current_exception` so that when the k module raises the + // QError it can provide the Python exception as cause via + // `raise QError from _current_exception[threading.getident()]`. + // We use a dict to track exceptions on a per-thread basis. Once the exception has been used + // by the k module, it is set to `None` in the `exception_tracker` dictionary. + P thread_id = PyObject_CallFunction(thread_get_ident, NULL); + if (PyDict_SetItem(exception_tracker, thread_id, ex_value) && PyErr_Occurred()) { + PyErr_WriteUnraisable(ex_value); + } + if (ex_value) + Py_XDECREF(ex_value); + if (ex_traceback) + Py_XDECREF(ex_traceback); + if (thread_id) + Py_XDECREF(thread_id); + return k; +} + + +static P k_to_py_cast(K x, K typenum, K israw) { + if (x->t == 112) { + return get_py_ptr(x); + } + + P factory_args = PyTuple_New(4); + PyTuple_SetItem(factory_args, 0, Py_BuildValue("K", (unsigned long long)x)); + PyTuple_SetItem(factory_args, 1, Py_True); + PyTuple_SetItem(factory_args, 2, Py_BuildValue("l", (long)typenum->j)); + if (israw->g) { + PyTuple_SetItem(factory_args, 3, Py_True); + Py_INCREF(Py_True); + } else { + PyTuple_SetItem(factory_args, 3, Py_False); + Py_INCREF(Py_False); + } + Py_INCREF(Py_True); + + P new_val = PyObject_CallObject(pyfactory, factory_args); + Py_XDECREF(factory_args); + + return new_val; +} + + +static P k_to_py_list(K x) { + if (x->t == 112) { + return get_py_ptr(x); + } + + P factory_args = PyTuple_New(2); + PyTuple_SetItem(factory_args, 0, Py_BuildValue("K", (unsigned long long)x)); + PyTuple_SetItem(factory_args, 1, Py_True); + Py_INCREF(Py_True); + + P new_val = (P)PyObject_CallObject(k_factory, factory_args); + Py_XDECREF(factory_args); + return new_val; +} + + + + +K _k_pyrun(K k_ret, K k_eval_or_exec, K as_foreign, K k_code_string); +// k_eval_or_exec == 0 -> eval the code string +// k_eval_or_exec == 1 -> exec the code string +EXPORT K k_pyrun(K k_ret, K k_eval_or_exec, K as_foreign, K k_code_string) { + if (!setup) { + setup = true; + int gstate = PyGILState_Ensure(); + K res = _k_pyrun(k_ret, k_eval_or_exec, as_foreign, k_code_string); + PyGILState_Release(gstate); + return res; + } + struct CallNode* node = create_node(K_PY_RUN); + node->call->arg1 = k_ret; + node->call->arg2 = k_eval_or_exec; + node->call->arg3 = as_foreign; + node->call->arg4 = k_code_string; + pthread_mutex_lock(&head_mutex); + if (calls_head == NULL) { + calls_head = node; + calls_tail = node; + } else { + calls_tail->next = node; + calls_tail = node; + } + pthread_mutex_unlock(&head_mutex); + while (1 == 1) { + pthread_mutex_lock(&cond_mutex); + pthread_cond_signal(&cond); + pthread_mutex_unlock(&cond_mutex); + if (node->call->done) { + K res = node->call->result; + pthread_mutex_lock(&head_mutex); + delete_node(node); + pthread_mutex_unlock(&head_mutex); + return res; + } + } + return (K)0; +} +K _k_pyrun(K k_ret, K k_eval_or_exec, K as_foreign, K k_code_string) { + K k; + + // If a char atom is provided instead of a char vector, make it into a char vector: + if (k_code_string->t == -10) { + char str[1] = {k_code_string->g}; + k_code_string = kpn(str, 1); + } + + if (k_code_string->t != 10) { + return raise_k_error("String input expected for code evaluation/execution."); + } + + void* code_string = PyMem_Calloc(k_code_string->n + 1, 1); + strncpy(code_string, (const char *)k_code_string->G0, k_code_string->n); + P ctx = PyModule_GetDict(PyImport_AddModule("__main__")); + P py_ret = PyRun_String( + code_string, + k_eval_or_exec->g ? Py_file_input : Py_eval_input, + ctx, + ctx + ); + PyMem_Free(code_string); + + if (!k_ret->g) { + if ((k = k_py_error())) { + flush_stdout(); + Py_XDECREF(py_ret); + return k; + } else Py_XDECREF(py_ret); + flush_stdout(); + return (K)0; + } + if ((k = k_py_error())) { + flush_stdout(); + Py_XDECREF(py_ret); + return k; + } + + if (as_foreign->g) { + k = (K)create_foreign(py_ret); + flush_stdout(); + Py_XDECREF(py_ret); + return k; + } + P py_k_ret = PyObject_CallFunctionObjArgs(toq, py_ret, NULL); + Py_XDECREF(py_ret); + if ((k = k_py_error())) { + flush_stdout(); + Py_XDECREF(py_k_ret); + return k; + } + P py_addr = PyObject_GetAttrString(py_k_ret, "_addr"); + Py_XDECREF(py_k_ret); + k = (K)PyLong_AsLongLong(py_addr); + Py_XDECREF(py_addr); + flush_stdout(); + return k; +} + + +inline long long modpow(long long base, long long exp, long long mod) { + long long result = 1; + while (exp > 0) { + if (exp & 1) result = (result * base) % mod; + base = (base * base) % mod; + exp >>= 1; + } + return result; +} + + +EXPORT K k_modpow(K k_base, K k_exp, K k_mod_arg) { + K result; + K k_mod; + + if (k_mod_arg->t == 101 && k_mod_arg->g == 0) { + k_mod = kj(9223372036854775807); // default modulo is 2^63-1 + } + else { + k_mod = r1(k_mod_arg); + } + + if (k_base->t >= 0 && k_exp->t >= 0 && k_mod->t >= 0) { + if (k_base->n != k_exp->n || k_exp->n != k_mod->n) { + result = ks("length"); + result->t = -128; + } else { + result = ktn(7, k_base->n); + for (long long x = 0; x < k_base->n; ++x) kJ(result)[x] = modpow(kJ(k_base)[x], kJ(k_exp)[x], kJ(k_mod)[x]); + } + } else if (k_base->t >= 0 && k_exp->t >= 0) { + if (k_base->n != k_exp->n) { + result = ks("length"); + result->t = -128; + } + else { + result = ktn(7, k_base->n); + for (long long x = 0; x < k_base->n; ++x) kJ(result)[x] = modpow(kJ(k_base)[x], kJ(k_exp)[x], k_mod->j); + } + } else if (k_base->t >= 0 && k_mod->t >= 0) { + if (k_base->n != k_mod->n) { + result = ks("length"); + result->t = -128; + } + result = ktn(7, k_base->n); + for (long long x = 0; x < k_base->n; ++x) kJ(result)[x] = modpow(kJ(k_base)[x], k_exp->j, kJ(k_mod)[x]); + } else if (k_base->t >= 0) { + result = ktn(7, k_base->n); + for (long long x = 0; x < k_base->n; ++x) kJ(result)[x] = modpow(kJ(k_base)[x], k_exp->j, k_mod->j); + } else if (k_exp->t >= 0 && k_mod->t >= 0) { + if (k_exp->n != k_mod->n) { + result = ks("length"); + result->t = -128; + } + else { + result = ktn(7, k_exp->n); + for (long long x = 0; x < k_exp->n; ++x) kJ(result)[x] = modpow(k_base->j, kJ(k_exp)[x], kJ(k_mod)[x]); + } + } else if (k_exp->t >= 0) { + result = ktn(7, k_exp->n); + for (long long x = 0; x < k_exp->n; ++x) kJ(result)[x] = modpow(k_base->j, kJ(k_exp)[x], k_mod->j); + } else if (k_mod->t >= 0) { + result = ktn(7, k_mod->n); + for (long long x = 0; x < k_mod->n; ++x) kJ(result)[x] = modpow(k_base->j, k_exp->j, kJ(k_mod)[x]); + } else { + result = kj(modpow(k_base->j, k_exp->j, k_mod->j)); + } + r0(k_mod); + return result; +} + + +EXPORT K foreign_to_q(K f, K b, K a) { + struct CallNode* node = create_node(FOREIGN_TO_Q); + node->call->arg1 = f; + node->call->arg2 = b; + node->call->arg3 = a; + pthread_mutex_lock(&head_mutex); + if (calls_head == NULL) { + calls_head = node; + calls_tail = node; + } else { + calls_tail->next = node; + calls_tail = node; + } + pthread_mutex_unlock(&head_mutex); + while (1 == 1) { + pthread_mutex_lock(&cond_mutex); + pthread_cond_signal(&cond); + pthread_mutex_unlock(&cond_mutex); + if (node->call->done) { + K res = node->call->result; + pthread_mutex_lock(&head_mutex); + delete_node(node); + pthread_mutex_unlock(&head_mutex); + return res; + } + } + return (K)0; +} +K _foreign_to_q(K f, K b, K a) { + if (f->t != 112) + return raise_k_error("Expected foreign object for call to .pykx.toq"); + if (!check_py_foreign(f)) + return raise_k_error("Provided foreign object is not a Python object"); + K k; + + P pyobj = get_py_ptr(f); + Py_INCREF(pyobj); + + P toq_args = PyTuple_New(2); + PyTuple_SetItem(toq_args, 0, pyobj); + PyTuple_SetItem(toq_args, 1, Py_BuildValue("")); + + P _kwargs = PyDict_New(); + PyDict_SetItemString(_kwargs, "strings_as_char", PyBool_FromLong((long)b->g)); + PyDict_SetItemString(_kwargs, "no_allocator", PyBool_FromLong((long)a->g)); + + P qpy_val = PyObject_Call(toq, toq_args, _kwargs); + if ((k = k_py_error())) { + return k; + } + + P k_addr = PyObject_GetAttrString(qpy_val, "_addr"); + if ((k = k_py_error())) { + Py_XDECREF(toq_args); + Py_XDECREF(_kwargs); + Py_XDECREF(k_addr); + Py_XDECREF(qpy_val); + return k; + } + + long long _addr = PyLong_AsLongLong(k_addr); + K res = (K)(uintptr_t)_addr; + r1(res); + Py_XDECREF(toq_args); + Py_XDECREF(_kwargs); + Py_XDECREF(qpy_val); + Py_XDECREF(k_addr); + + return res; +} + +EXPORT K repr(K as_repr, K f) { + struct CallNode* node = create_node(REPR); + node->call->arg1 = as_repr; + node->call->arg2 = f; + pthread_mutex_lock(&head_mutex); + if (calls_head == NULL) { + calls_head = node; + calls_tail = node; + } else { + calls_tail->next = node; + calls_tail = node; + } + pthread_mutex_unlock(&head_mutex); + while (1 == 1) { + pthread_mutex_lock(&cond_mutex); + pthread_cond_signal(&cond); + pthread_mutex_unlock(&cond_mutex); + if (node->call->done) { + K res = node->call->result; + pthread_mutex_lock(&head_mutex); + delete_node(node); + pthread_mutex_unlock(&head_mutex); + return res; + } + } + return (K)0; +} +K _repr(K as_repr, K f) { + K k; + if (f->t != 112) { + if (as_repr->g){ + if (f->t == 105) { + return raise_k_error("Expected a foreign object for .pykx.repr, try unwrapping the foreign object with `."); + } + return raise_k_error("Expected a foreign object for .pykx.repr"); + } else { + if (f->t == 105) { + return raise_k_error("Expected a foreign object for .pykx.print, try unwrapping the foreign object with `."); + } + return raise_k_error("Expected a foreign object for .pykx.print"); + } + } + else { + if (!check_py_foreign(f)) + return raise_k_error("Provided foreign object is not a Python object"); + } + P repr; + P str; + P p = get_py_ptr(f); + repr = PyObject_Repr(p); + str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~"); + Py_XDECREF(repr); + if (!as_repr->g) { + const char *bytes = PyBytes_AS_STRING(str); + PySys_WriteStdout("%s\n", bytes); + flush_stdout(); + Py_XDECREF(str); + return (K)0; + } + if ((k = k_py_error())) { + flush_stdout(); + Py_XDECREF(str); + return k; + } + flush_stdout(); + const char *chars = PyBytes_AS_STRING(str); + return kp(chars); +} + +EXPORT K get_attr(K f, K attr) { + struct CallNode* node = create_node(GET_ATTR); + node->call->arg1 = f; + node->call->arg2 = attr; + pthread_mutex_lock(&head_mutex); + if (calls_head == NULL) { + calls_head = node; + calls_tail = node; + } else { + calls_tail->next = node; + calls_tail = node; + } + pthread_mutex_unlock(&head_mutex); + while (1 == 1) { + pthread_mutex_lock(&cond_mutex); + pthread_cond_signal(&cond); + pthread_mutex_unlock(&cond_mutex); + if (node->call->done) { + K res = node->call->result; + pthread_mutex_lock(&head_mutex); + delete_node(node); + pthread_mutex_unlock(&head_mutex); + return res; + } + } + return (K)0; +} +K _get_attr(K f, K attr) { + K k; + if (f->t != 112) { + if (f->t == 105) { + return raise_k_error("Expected foreign object for call to .pykx.getattr, try unwrapping the foreign object with `."); + } + return raise_k_error("Expected foreign object for call to .pykx.getattr"); + } + if (attr->t != -11) { + return raise_k_error("Expected a SymbolAtom for the attribute to get in .pykx.getattr"); + } + P p = get_py_ptr(f); + P _attr = Py_BuildValue("s", attr->s); + K res = create_foreign(PyObject_GetAttr(p, _attr)); + Py_XDECREF(_attr); + if ((k = k_py_error())) { + return k; + } + return res; +} + +EXPORT K get_global(K attr) { + struct CallNode* node = create_node(GET_GLOBAL); + node->call->arg1 = attr; + pthread_mutex_lock(&head_mutex); + if (calls_head == NULL) { + calls_head = node; + calls_tail = node; + } else { + calls_tail->next = node; + calls_tail = node; + } + pthread_mutex_unlock(&head_mutex); + while (1 == 1) { + pthread_mutex_lock(&cond_mutex); + pthread_cond_signal(&cond); + pthread_mutex_unlock(&cond_mutex); + if (node->call->done) { + K res = node->call->result; + pthread_mutex_lock(&head_mutex); + delete_node(node); + pthread_mutex_unlock(&head_mutex); + return res; + } + } + return (K)0; +} +K _get_global(K attr) { + K k; + if (attr->t != -11) { + return raise_k_error("Expected a SymbolAtom for the attribute to get in .pykx.get"); + } + P p = PyImport_AddModule("__main__"); + if ((k = k_py_error())) { + return k; + } + P _attr = Py_BuildValue("s", attr->s); + K res = create_foreign(PyObject_GetAttr(p, _attr)); + Py_XDECREF(_attr); + if ((k = k_py_error())) { + return k; + } + return res; +} + + +EXPORT K set_global(K attr, K val) { + struct CallNode* node = create_node(SET_GLOBAL); + node->call->arg1 = attr; + node->call->arg2 = val; + pthread_mutex_lock(&head_mutex); + if (calls_head == NULL) { + calls_head = node; + calls_tail = node; + } else { + calls_tail->next = node; + calls_tail = node; + } + pthread_mutex_unlock(&head_mutex); + while (1 == 1) { + pthread_mutex_lock(&cond_mutex); + pthread_cond_signal(&cond); + pthread_mutex_unlock(&cond_mutex); + if (node->call->done) { + K res = node->call->result; + pthread_mutex_lock(&head_mutex); + delete_node(node); + pthread_mutex_unlock(&head_mutex); + return res; + } + } + return (K)0; +} +K _set_global(K attr, K val) { + K k; + + P p = PyImport_AddModule("__main__"); + if ((k = k_py_error())) { + return k; + } + P v = get_py_ptr(val); + if ((k = k_py_error())) { + return k; + } + PyObject_SetAttrString(p, attr->s, v); + if ((k = k_py_error())) { + return k; + } + return NULL; +} + +EXPORT K set_attr(K f, K attr, K val) { + struct CallNode* node = create_node(SET_ATTR); + node->call->arg1 = f; + node->call->arg2 = attr; + node->call->arg3 = val; + pthread_mutex_lock(&head_mutex); + if (calls_head == NULL) { + calls_head = node; + calls_tail = node; + } else { + calls_tail->next = node; + calls_tail = node; + } + pthread_mutex_unlock(&head_mutex); + while (1 == 1) { + pthread_mutex_lock(&cond_mutex); + pthread_cond_signal(&cond); + pthread_mutex_unlock(&cond_mutex); + if (node->call->done) { + K res = node->call->result; + pthread_mutex_lock(&head_mutex); + delete_node(node); + pthread_mutex_unlock(&head_mutex); + return res; + } + } + return (K)0; +} +K _set_attr(K f, K attr, K val) { + if (f->t != 112) { + if (f->t == 105) { + return raise_k_error("Expected foreign object for call to .pykx.setattr, try unwrapping the foreign object with `."); + } + return raise_k_error("Expected foreign object for call to .pykx.setattr"); + } + else { + if (!check_py_foreign(f)) + return raise_k_error("Provided foreign object is not a Python object, not suitable to have an attribute set"); + } + if (attr->t != -11) { + return raise_k_error("Expected a SymbolAtom for the attribute to set in .pykx.setattr"); + } + K k; + P p = get_py_ptr(f); + Py_INCREF(p); + P v = get_py_ptr(val); + if ((k = k_py_error())) { + return k; + } + PyObject_SetAttrString(p, attr->s, v); + if ((k = k_py_error())) { + return k; + } + return NULL; +} + +K _import(K module); +EXPORT K import(K module) { + if (!import_setup) { + import_setup = true; + int gstate = PyGILState_Ensure(); + K res = _import(module); + PyGILState_Release(gstate); + return res; + } + struct CallNode* node = create_node(IMPORT); + node->call->arg1 = module; + pthread_mutex_lock(&head_mutex); + if (calls_head == NULL) { + calls_head = node; + calls_tail = node; + } else { + calls_tail->next = node; + calls_tail = node; + } + pthread_mutex_unlock(&head_mutex); + while (1 == 1) { + pthread_mutex_lock(&cond_mutex); + pthread_cond_signal(&cond); + pthread_mutex_unlock(&cond_mutex); + if (node->call->done) { + K res = node->call->result; + pthread_mutex_lock(&head_mutex); + delete_node(node); + pthread_mutex_unlock(&head_mutex); + return res; + } + } + return (K)0; +} +K _import(K module) { + K k; + K res; + if (module->t != -11) + return raise_k_error("Module to be imported must be a symbol"); + + P p = PyImport_ImportModule(module->s); + if ((k = k_py_error())) { + return k; + } + res = create_foreign(p); + return res; +} + + + +K k(int, const char*, ...); +EXPORT K call_func(K f, K has_no_args, K args, K kwargs) { + struct CallNode* node = create_node(CALL); + node->call->arg1 = r1(f); + node->call->arg2 = r1(has_no_args); + node->call->arg3 = r1(args); + node->call->arg4 = r1(kwargs); + //node->call->arg1 = f; + //node->call->arg2 = has_no_args; + //node->call->arg3 = args; + //node->call->arg4 = kwargs; + pthread_mutex_lock(&head_mutex); + if (calls_head == NULL) { + calls_head = node; + calls_tail = node; + } else { + calls_tail->next = node; + calls_tail = node; + } + pthread_mutex_unlock(&head_mutex); + while (1 == 1) { + pthread_mutex_lock(&cond_mutex); + pthread_cond_signal(&cond); + pthread_mutex_unlock(&cond_mutex); + if (node->call->done) { + K res = node->call->result; + pthread_mutex_lock(&head_mutex); + delete_node(node); + pthread_mutex_unlock(&head_mutex); + return res; + } + } + return (K)0; +} +K _call_func(K f, K has_no_args, K args, K kwargs) { + K k_; + P pyf = NULL; + + if ((k_ = k_py_error())) { + return k_; + } + pyf = get_py_ptr(f); + Py_INCREF(pyf); + if (!PyCallable_Check(pyf)) { + return raise_k_error("Attempted to call non callable python foreign object"); + } + + int len = (has_no_args->j==0)?0:(int)args->n; + P py_params = NULL; + P py_kwargs = NULL; + if (len != 0) { + py_params = k_to_py_list(args); + if ((k_ = k_py_error())) { + Py_XDECREF(py_params); + return k_; + } + } else + py_params = PyTuple_New(0); + + if ((kK(kwargs)[0])->n != 0) { + P factory_args = PyTuple_New(1); + PyTuple_SetItem(factory_args, 0, Py_BuildValue("K", (unsigned long long)kwargs)); + if ((k_ = k_py_error())) { + Py_XDECREF(py_params); + Py_XDECREF(py_kwargs); + Py_XDECREF(factory_args); + return k_; + } + py_kwargs = PyObject_CallObject(k_dict_converter, factory_args); + Py_XDECREF(factory_args); + + if ((k_ = k_py_error())) { + Py_XDECREF(py_params); + Py_XDECREF(py_kwargs); + return k_; + } + } + P pyres = PyObject_Call(pyf, py_params, py_kwargs); + Py_XDECREF(pyf); + Py_XDECREF(py_params); + Py_XDECREF(py_kwargs); + + if ((k_ = k_py_error())) { + if (pyres) { + Py_XDECREF(pyres); + } + flush_stdout(); + return k_; + } + K res; + if (pyres == NULL) { + pyres = Py_BuildValue(""); + } + + res = create_foreign(pyres); + Py_XDECREF(pyres); + flush_stdout(); + return res; +} + +EXPORT K k_to_py_foreign(K x, K typenum, K israw) { + struct CallNode* node = create_node(K_PY_FOREIGN); + r1(x); + node->call->arg1 = x; + node->call->arg2 = typenum; + node->call->arg3 = israw; + pthread_mutex_lock(&head_mutex); + if (calls_head == NULL) { + calls_head = node; + calls_tail = node; + } else { + calls_tail->next = node; + calls_tail = node; + } + pthread_mutex_unlock(&head_mutex); + while (1 == 1) { + pthread_mutex_lock(&cond_mutex); + pthread_cond_signal(&cond); + pthread_mutex_unlock(&cond_mutex); + if (node->call->done) { + K res = node->call->result; + pthread_mutex_lock(&head_mutex); + delete_node(node); + pthread_mutex_unlock(&head_mutex); + return res; + } + } + return (K)0; +} +K _k_to_py_foreign(K x, K typenum, K israw) { + K kx; + P p = k_to_py_cast(x, typenum, israw); + if ((kx = k_py_error())) { + return kx; + } + kx = create_foreign(p); + Py_XDECREF(p); + return kx; +} + +void* thread_init(void* _init_args) { + while (1 == 1) { + pthread_mutex_lock(&cond_mutex); + while (calls_head == NULL && kill_thread == false) { + pthread_cond_wait(&cond, &cond_mutex); + } + pthread_mutex_unlock(&cond_mutex); + if (kill_thread) { + break; + } + pthread_mutex_lock(&head_mutex); + if (calls_head != NULL) { + struct CallArgs* call = calls_head->call; + void* t = PyEval_SaveThread(); + int gstate = PyGILState_Ensure(); + switch (call->func) { + case REPR: + call->result = _repr(call->arg1, call->arg2); + call->done = true; + break; + case GET_ATTR: + call->result = _get_attr(call->arg1, call->arg2); + call->done = true; + break; + case GET_GLOBAL: + call->result = _get_global(call->arg1); + call->done = true; + break; + case SET_ATTR: + call->result = _set_attr(call->arg1, call->arg2, call->arg3); + call->done = true; + break; + case SET_GLOBAL: + call->result = _set_global(call->arg1, call->arg2); + call->done = true; + break; + case K_PY_RUN: + call->result = _k_pyrun(call->arg1, call->arg2, call->arg3, call->arg4); + call->done = true; + break; + case K_PY_FOREIGN: + call->result = _k_to_py_foreign(call->arg1, call->arg2, call->arg3); + call->done = true; + break; + case FOREIGN_TO_Q: + call->result = _foreign_to_q(call->arg1, call->arg2, call->arg3); + call->done = true; + break; + case IMPORT: + call->result = _import(call->arg1); + call->done = true; + break; + case CALL: + call->result = _call_func(call->arg1, call->arg2, call->arg3, call->arg4); + call->done = true; + break; + } + PyGILState_Release(gstate); + PyEval_RestoreThread(t); + calls_head = calls_head->next; + } + pthread_mutex_unlock(&head_mutex); + } + pthread_exit(0); + return NULL; +} diff --git a/src/pykx/q.so/libs/4-1/l64/libq.so b/src/pykx/q.so/libs/4-1/l64/libq.so index 751fb46..9402e6a 100755 Binary files a/src/pykx/q.so/libs/4-1/l64/libq.so and b/src/pykx/q.so/libs/4-1/l64/libq.so differ diff --git a/src/pykx/q.so/libs/4-1/l64arm/libq.so b/src/pykx/q.so/libs/4-1/l64arm/libq.so index 9326a28..0fa82bc 100755 Binary files a/src/pykx/q.so/libs/4-1/l64arm/libq.so and b/src/pykx/q.so/libs/4-1/l64arm/libq.so differ diff --git a/src/pykx/q.so/libs/4-1/m64/libq.dylib b/src/pykx/q.so/libs/4-1/m64/libq.dylib index 402ef2f..548949f 100755 Binary files a/src/pykx/q.so/libs/4-1/m64/libq.dylib and b/src/pykx/q.so/libs/4-1/m64/libq.dylib differ diff --git a/src/pykx/q.so/libs/4-1/m64arm/libq.dylib b/src/pykx/q.so/libs/4-1/m64arm/libq.dylib index 41bb290..4088297 100755 Binary files a/src/pykx/q.so/libs/4-1/m64arm/libq.dylib and b/src/pykx/q.so/libs/4-1/m64arm/libq.dylib differ diff --git a/src/pykx/q.so/libs/4-1/w64/q.dll b/src/pykx/q.so/libs/4-1/w64/q.dll index b3e1203..c679126 100644 Binary files a/src/pykx/q.so/libs/4-1/w64/q.dll and b/src/pykx/q.so/libs/4-1/w64/q.dll differ diff --git a/src/pykx/q.so/qk/pykx_init.q_ b/src/pykx/q.so/qk/pykx_init.q_ index c312fca..7d27e91 100644 Binary files a/src/pykx/q.so/qk/pykx_init.q_ and b/src/pykx/q.so/qk/pykx_init.q_ differ diff --git a/src/pykx/tick.py b/src/pykx/tick.py index 26e8030..89379bd 100644 --- a/src/pykx/tick.py +++ b/src/pykx/tick.py @@ -38,7 +38,6 @@ class STREAMING: """ The `STREAMING` class acts as a base parent class for the TICK, RTP, HDB and GATEWAY class objects. Each of these child classes inherit and may modify the logic of this parent. - class objects. Each of these child classes inherit and may modify the logic of this parent. In all cases the functions `libraries` and `register_api` for example have the same definition and are available to all process types. diff --git a/src/pykx/toq.pyx b/src/pykx/toq.pyx index 2bb87c5..a9ef043 100644 --- a/src/pykx/toq.pyx +++ b/src/pykx/toq.pyx @@ -66,7 +66,7 @@ x y | | | Numpy arrays. | | +------------------+---------------------------+---------------------------------------+-------------+ | `no_allocator | `bool` | When used the conversion will not use | `False` | -| | | the `PYKX_ALLOCATOR` behaviour. | | +| | | the `PYKX_ALLOCATOR` behavior. | | +------------------+---------------------------+---------------------------------------+-------------+ | `strings_as_char | `bool` | When used all Python `str` objects | `False` | | | | are converted to `pykx.CharVector` | | diff --git a/src/pykx/wrappers.py b/src/pykx/wrappers.py index ce87bc6..1d44d04 100644 --- a/src/pykx/wrappers.py +++ b/src/pykx/wrappers.py @@ -3927,7 +3927,7 @@ def xbar(self, values): def window_join(self, table, windows, cols, aggs): """ - Window joins provide the ability to analyse the behaviour of data + Window joins provide the ability to analyse the behavior of data in one table in the neighborhood of another. Parameters: diff --git a/tests/test_license.py b/tests/test_license.py index f8359ff..db56286 100644 --- a/tests/test_license.py +++ b/tests/test_license.py @@ -304,7 +304,7 @@ def test_use_both_licensed_and_unlicensed_flags(QARGS): def test_env_combination(): os.environ['QARGS'] = '--licensed' os.environ['PYKX_UNLICENSED'] = 'true' - with pytest.raises(Exception, match="(?i)'licensed' and 'unlicensed' behaviour"): + with pytest.raises(Exception, match="(?i)'licensed' and 'unlicensed' behavior"): import pykx # noqa: F401 diff --git a/tests/test_pandas_api.py b/tests/test_pandas_api.py index 94971eb..1d8c3a7 100644 --- a/tests/test_pandas_api.py +++ b/tests/test_pandas_api.py @@ -935,6 +935,25 @@ def test_table_outer_merge(kx, q): assert df_res.equals(res) +def test_merge_sort(kx, q): + d = kx.q.z.D + a = kx.toq(pd.DataFrame({'r': [2, 3, 4], 'date': [d, d-3, d-1]})) + b = kx.toq(pd.DataFrame({'r': [5, 6, 7], 'date': [d-1, d, d-3]})) + res = a.merge(b, how="outer", on="date", sort=True) + assert all(res[0]['date'] == kx.q('enlist', d-3)) + + +def test_merge_sort_single_key(kx, q): + d = kx.q.z.D + a = kx.toq(pd.DataFrame({'r': [2, 3, 4], 'date': [d, d-3, d-1]})) + b = kx.toq(pd.DataFrame({'r': [5, 6, 7], 'date': [d-1, d, d-3]})) + a = kx.q.xkey('date', a) + res = a.merge(b, how="outer", on="date", sort=True) + assert all(res.keys().columns == ['date']) + res = kx.q('0!', res) + assert all(res[0]['date'] == kx.q('enlist', d-3)) + + def test_cross_merge(kx, q): df1 = pd.DataFrame({'lkey': ['foo', 'bar', 'baz', 'foo'], 'value': [1, 2, 3, 5]}) df2 = pd.DataFrame({'rkey': ['foo', 'bar', 'baz', 'foo'], 'value': [5, 6, 7, 8]}) diff --git a/tests/test_pykx.py b/tests/test_pykx.py index c66133d..f8bde85 100644 --- a/tests/test_pykx.py +++ b/tests/test_pykx.py @@ -254,7 +254,7 @@ def test_pykx_sigkill(): @pytest.mark.isolate -@pytest.mark.xfail(reason='Local testing shows appropriate behaviour') +@pytest.mark.xfail(reason='Local testing shows appropriate behavior') def test_pykx_sigint(): output = subprocess.run( (str(Path(sys.executable).as_posix()), '-c', 'import pykx as kx; import os; import signal; pid = os.getpid(); os.kill(pid, signal.SIGINT)'), # noqa: E501 @@ -448,31 +448,31 @@ def test_kx_versions(kx): if test_vars == ('Linux', 'x86_64', '4.0'): assert kx.q.z.k == kx.q('2025.02.18') elif test_vars == ('Linux', 'x86_64', '4.1'): - assert kx.q.z.k == kx.q('2025.04.28') + assert kx.q.z.k == kx.q('2025.11.25') # elif test_vars == ('Linux', 'x86_64', '4.2'): # assert kx.q.z.k == kx.q('?') elif test_vars == ('Linux', 'aarch64', '4.0'): assert kx.q.z.k == kx.q('2025.02.18') elif test_vars == ('Linux', 'aarch64', '4.1'): - assert kx.q.z.k == kx.q('2025.04.28') + assert kx.q.z.k == kx.q('2025.11.25') # elif test_vars == ('Linux', 'aarch64', '4.2'): # assert kx.q.z.k == kx.q('?') elif test_vars == ('Darwin', 'x86_64', '4.0'): assert kx.q.z.k == kx.q('2025.02.18') elif test_vars == ('Darwin', 'x86_64', '4.1'): - assert kx.q.z.k == kx.q('2025.04.28') + assert kx.q.z.k == kx.q('2025.11.25') # elif test_vars == 'Darwin', 'x86_64', '4.2'): # assert kx.q.z.k == kx.q('?') elif test_vars == ('Darwin', 'arm64', '4.0'): assert kx.q.z.k == kx.q('2025.02.18') elif test_vars == ('Darwin', 'arm64', '4.1'): - assert kx.q.z.k == kx.q('2025.04.28') + assert kx.q.z.k == kx.q('2025.11.25') # elif test_vars == ('Darwin', 'arm', '4.2'): # assert kx.q.z.k == kx.q('?') elif test_vars == ('Windows', 'AMD64', '4.0'): assert kx.q.z.k == kx.q('2025.02.18') elif test_vars == ('Windows', 'AMD64', '4.1'): - assert kx.q.z.k == kx.q('2025.04.28') + assert kx.q.z.k == kx.q('2025.11.25') # elif test_vars == ('Windows', 'AMD64', '4.2'): # assert kx.q.z.k == kx.q('?') else: diff --git a/tests/test_q_future.py b/tests/test_q_future.py index a354fb9..0b5c319 100644 --- a/tests/test_q_future.py +++ b/tests/test_q_future.py @@ -27,7 +27,7 @@ async def test_internal_await(kx, q_port, event_loop): @pytest.mark.asyncio @pytest.mark.unlicensed @pytest.mark.xfail( - reason="Super flaky with all the different behaviours of futures between asyncio versions." + reason="Super flaky with all the different behaviors of futures between asyncio versions." ) async def test_q_future_callbacks(kx, q_port): def _callback(x): @@ -59,7 +59,7 @@ def _callback(x): @pytest.mark.asyncio @pytest.mark.unlicensed @pytest.mark.xfail( - reason="Super flaky with all the different behaviours of futures between asyncio versions." + reason="Super flaky with all the different behaviors of futures between asyncio versions." ) async def test_q_future_errors(kx, q_port): def foo():