From 34c96d8cec49ce656aea10f7d34435a69b966a17 Mon Sep 17 00:00:00 2001
From: Rob Nagler <5495179+robnagler@users.noreply.github.com>
Date: Sun, 26 Jan 2025 21:31:03 +0000
Subject: [PATCH 1/6] ckp
---
pykern/http_unit.py | 199 ++++++++++++++++++++++++++++++++------------
1 file changed, 147 insertions(+), 52 deletions(-)
diff --git a/pykern/http_unit.py b/pykern/http_unit.py
index 40908280..a3f18c1c 100644
--- a/pykern/http_unit.py
+++ b/pykern/http_unit.py
@@ -4,80 +4,175 @@
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
-# Defer imports for unit tests
+# Defer as many pykern imports as possible to defer pkconfig runing
+from pykern.pkcollections import PKDict
+import os
+import signal
+import time
class Setup:
+ """Usage::
+
+ async with http_unit.Setup(api_classes=(_class())) as c:
+ from pykern.pkcollections import PKDict
+ from pykern import pkunit
+
+ e = PKDict(ping="pong")
+ pkunit.pkeq(e.pkupdate(counter=1), await c.call_api("echo", e))
+ pkunit.pkeq(e.pkupdate(counter=2), await c.call_api("echo", e))
+
+ May be subclassed to start multiple servers.
+ """
AUTH_TOKEN = "http_unit_auth_secret"
- def __init__(self, api_classes, attr_classes=(), coros=()):
- import os, time
- from pykern.pkcollections import PKDict
+ def __init__(self, **server_config):
+ # Must be first
+ self._global_config()
+ self.http_config = self._http_config()
+ self.server_config = self._server_config(server_config)
+ self.server_pid = self._server_process()
+ time.sleep(1)
+ self.client = self._client()
- def _global_config():
- c = PKDict(
- PYKERN_PKDEBUG_WANT_PID_TIME="1",
- )
- os.environ.update(**c)
- from pykern import pkconfig
+ def destroy(self):
+ """Destroy client and kill attributes ``*_pid`` attrs"""
- pkconfig.reset_state_for_testing(c)
+ self.client.destroy()
+ for p in filter(lambda x: x.endswith("_pid"), dir(self)):
+ try:
+ os.kill(getattr(self, p), signal.SIGKILL)
+ except Exception:
+ pass
- def _http_config():
- from pykern import pkconst, pkunit
+ def _client(self):
+ """Creates a client to be used for requests.
- return PKDict(
- # any uri is fine
- api_uri="/http_unit",
- # just needs to be >= 16 word (required by http) chars; apps should generate this randomly
- tcp_ip=pkconst.LOCALHOST_IP,
- tcp_port=pkunit.unbound_localhost_tcp_port(),
- )
+ Called in `__init__`.
- def _server():
- from pykern import pkdebug, http
+ Returns:
+ object: http client, set to ``self.client``
+ """
+ from pykern import http, pkdebug
- def _api_classes():
- class AuthAPI(http.AuthAPI):
- PYKERN_HTTP_TOKEN = self.AUTH_TOKEN
+ pkdebug.pkdp(self.http_config)
+ return http.HTTPClient(self.http_config.copy())
- return list(api_classes) + [AuthAPI]
+ def _client_awaitable(self):
+ """How to connect to client
+
+ Awaited in `__aenter__`.
+
+ Returns:
+ Awaitable: coroutine to connect to client
+ """
- if rv := os.fork():
- return rv
- try:
- pkdebug.pkdlog("start server")
- http.server_start(
- attr_classes=attr_classes,
- api_classes=_api_classes(),
- http_config=self.http_config.copy(),
- coros=coros,
- )
- except Exception as e:
- pkdebug.pkdlog("server exception={} stack={}", e, pkdebug.pkdexc())
- finally:
- os._exit(0)
-
- _global_config()
- self.http_config = _http_config()
- self.server_pid = _server()
- time.sleep(1)
from pykern import http
- self.client = http.HTTPClient(self.http_config.copy())
+ return self.client.connect(http.AuthArgs(token=self.AUTH_TOKEN))
- def destroy(self):
- import os, signal
+ def _global_config(self, **kwargs):
+ """Initializes os.environ and pkconfig
- os.kill(self.server_pid, signal.SIGKILL)
+ Called first.
- async def __aenter__(self):
+ Args:
+ kwargs (dict): merged into environ and config (from subclasses)
+ """
+
+ c = PKDict(
+ PYKERN_PKDEBUG_WANT_PID_TIME="1",
+ **kwargs,
+ )
+ os.environ.update(**c)
+ from pykern import pkconfig
+
+ pkconfig.reset_state_for_testing(c)
+
+ def _http_config(self):
+ """Initializes ``self.http_config``
+
+ Returns:
+ PKDict: configuration to be shared with client and server
+ """
+ from pykern import pkconst, pkunit
+
+ return PKDict(
+ # any uri is fine
+ api_uri="/http_unit",
+ # just needs to be >= 16 word (required by http) chars; apps should generate this randomly
+ tcp_ip=pkconst.LOCALHOST_IP,
+ tcp_port=pkunit.unbound_localhost_tcp_port(),
+ )
+
+ def _server_config(self, init_config):
+ """Config to be passed to `pykern.http.server_start` in `server_start`
+
+ A simple `pykern.http.AuthAPI` implementation is defaulted if not in ``init_config.api_classes``.
+
+ Args:
+ init_config (dict): what was passed to `__init__`
+ Returns:
+ PKDict: configuration for `server_start`
+ """
+
+ def _api_classes(init_classes):
+ from pykern import http
+
+ class AuthAPI(http.AuthAPI):
+ PYKERN_HTTP_TOKEN = self.AUTH_TOKEN
+
+ rv = init_classes
+ if any(filter(lambda c: issubclass(c, http.AuthAPI), rv)):
+ return rv
+ return rv + [AuthAPI]
+
+ rv = PKDict(init_config) if init_config else PKDict()
+ rv.pksetdefault(
+ api_classes=(),
+ attr_classes=(),
+ coros=(),
+ http_config=PKDict,
+ )
+ rv.http_config.pksetdefault(**self.http_config)
+ rv.api_classes = _api_classes(list(rv.api_classes))
+ return rv
+
+ def _server_process(self):
+ """Call `server_start` in separate process
+
+ Override this method to start multiple servers, saving pids in
+ attributes that end in ``_pid`` so that `destroy` will kill
+ them.
+
+ Returns:
+ int: pid of process
+
+ """
+ from pykern import pkdebug
+
+ pkdebug.pkdp(self.server_config)
+ if rv := os.fork():
+ return rv
+ try:
+ pkdebug.pkdlog("start server")
+ self._server_start()
+ except Exception as e:
+ pkdebug.pkdlog("{} exception={} stack={}", op, e, pkdebug.pkdexc())
+ finally:
+ os._exit(0)
+
+ def _server_start(self):
+ """Calls http.server_start with ``self.server_config``"""
from pykern import http
- await self.client.connect(http.AuthArgs(token=self.AUTH_TOKEN))
+ http.server_start(**self.server_config)
+
+ async def __aenter__(self):
+ await self._client_awaitable()
return self.client
async def __aexit__(self, *args, **kwargs):
- self.client.destroy()
+ self.destroy()
return False
From 8b37a96760448cb721b4052b48926a988cf9e742 Mon Sep 17 00:00:00 2001
From: Rob Nagler <5495179+robnagler@users.noreply.github.com>
Date: Mon, 27 Jan 2025 00:22:36 +0000
Subject: [PATCH 2/6] pykern/http_unit.py
---
pykern/http_unit.py | 12 ++++-----
pykern/pkcollections.py | 50 +++++++++++++++++++++++++++++++++++++
pykern/pkinspect.py | 3 ---
pykern/pkunit.py | 37 ++++++---------------------
pykern/util.py | 30 ++++++++++++++++++++--
tests/pkcollections_test.py | 35 +++++++++++++++++++++++---
6 files changed, 122 insertions(+), 45 deletions(-)
diff --git a/pykern/http_unit.py b/pykern/http_unit.py
index a3f18c1c..eeee3b24 100644
--- a/pykern/http_unit.py
+++ b/pykern/http_unit.py
@@ -33,7 +33,7 @@ def __init__(self, **server_config):
self.http_config = self._http_config()
self.server_config = self._server_config(server_config)
self.server_pid = self._server_process()
- time.sleep(1)
+ time.sleep(2)
self.client = self._client()
def destroy(self):
@@ -56,8 +56,7 @@ def _client(self):
"""
from pykern import http, pkdebug
- pkdebug.pkdp(self.http_config)
- return http.HTTPClient(self.http_config.copy())
+ return http.HTTPClient(pkdebug.pkdp(self.http_config.copy()))
def _client_awaitable(self):
"""How to connect to client
@@ -96,14 +95,14 @@ def _http_config(self):
Returns:
PKDict: configuration to be shared with client and server
"""
- from pykern import pkconst, pkunit
+ from pykern import pkconst, util
return PKDict(
# any uri is fine
api_uri="/http_unit",
# just needs to be >= 16 word (required by http) chars; apps should generate this randomly
tcp_ip=pkconst.LOCALHOST_IP,
- tcp_port=pkunit.unbound_localhost_tcp_port(),
+ tcp_port=util.unbound_localhost_tcp_port(),
)
def _server_config(self, init_config):
@@ -152,14 +151,13 @@ def _server_process(self):
"""
from pykern import pkdebug
- pkdebug.pkdp(self.server_config)
if rv := os.fork():
return rv
try:
pkdebug.pkdlog("start server")
self._server_start()
except Exception as e:
- pkdebug.pkdlog("{} exception={} stack={}", op, e, pkdebug.pkdexc())
+ pkdebug.pkdlog("exception={} stack={}", e, pkdebug.pkdexc())
finally:
os._exit(0)
diff --git a/pykern/pkcollections.py b/pykern/pkcollections.py
index 7d384433..8f4aae3c 100644
--- a/pykern/pkcollections.py
+++ b/pykern/pkcollections.py
@@ -14,6 +14,8 @@
import types
import pykern.pkcompat
+_READ_ONLY_ATTR = "_PKDict__read_only"
+
class PKDict(dict):
"""A subclass of dict that allows items to be read/written as attributes.
@@ -46,6 +48,10 @@ class PKDict(dict):
only, not general objects.
"""
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ super().__setattr__(_READ_ONLY_ATTR, False)
+
def __delattr__(self, name):
raise PKDictNameError("{}: you cannot delete attributes", name)
@@ -89,6 +95,7 @@ def pkdel(self, name, default=None):
Returns:
object: value (if exists) or default
"""
+ self.__assert_not_read_only()
try:
return self[name]
except KeyError:
@@ -99,6 +106,14 @@ def pkdel(self, name, default=None):
except KeyError:
pass
+ def pkis_read_only(self):
+ """Tests is read only attribute
+
+ Returns:
+ True: if attribute is read only
+ """
+ return getattr(self, _READ_ONLY_ATTR)
+
def pkmerge(self, to_merge, make_copy=True):
"""Add `to_merge` to `self`
@@ -200,6 +215,7 @@ def pknested_set(self, qualifiers, value):
Returns:
object: self
"""
+ self.__assert_not_read_only()
q = qualifiers.split(".") if isinstance(qualifiers, str) else list(qualifiers)
d = self
for k in q[:-1]:
@@ -227,6 +243,7 @@ def pksetdefault(self, *args, **kwargs):
Returns:
object: self
"""
+ self.__assert_not_read_only()
if args and kwargs:
raise AssertionError("one of args or kwargs must be set, but not both")
if args:
@@ -259,6 +276,7 @@ def pksetdefault1(self, *args, **kwargs):
object: ``self[key]``; either `value` if just set, or preexisting value
"""
+ self.__assert_not_read_only()
if args and kwargs:
raise AssertionError("one of args or kwargs must be set, but not both")
if args:
@@ -289,12 +307,38 @@ def pkunchecked_nested_get(self, qualifiers, default=None):
except (KeyError, IndexError, TypeError, ValueError):
return default
+ def __assert_not_read_only(self):
+ if getattr(self, _READ_ONLY_ATTR):
+ raise PKDictReadOnlyError()
+
+ def pkset_read_only(self):
+ """Makes the current instance readonly
+
+ Returns:
+ PKDict: self
+ """
+ if self.pkis_read_only():
+ return self
+ super().__setattr__(_READ_ONLY_ATTR, True)
+ for n in (
+ "__delitem__",
+ "__setitem__",
+ "clear",
+ "pop",
+ "popitem",
+ "setdefault" "update",
+ ):
+ super().__setattr__(n, self.__assert_not_read_only)
+ return self
+
def pkupdate(self, *args, **kwargs):
"""Call `dict.update` and return ``self``."""
+ self.__assert_not_read_only()
super(PKDict, self).update(*args, **kwargs)
return self
def __pksetdefault_one(self, key, value):
+ self.__assert_not_read_only()
if key not in self:
self[key] = value() if callable(value) else value
return self[key]
@@ -306,6 +350,12 @@ class PKDictNameError(NameError):
pass
+class PKDictReadOnlyError(RuntimeError):
+ """Raised when an attempt to write read-only PKDict"""
+
+ pass
+
+
def canonicalize(obj):
"""Convert to lists and PKDicts for simpler serialization
diff --git a/pykern/pkinspect.py b/pykern/pkinspect.py
index 9abd700a..b35e2dd7 100644
--- a/pykern/pkinspect.py
+++ b/pykern/pkinspect.py
@@ -1,11 +1,8 @@
-# -*- coding: utf-8 -*-
"""Helper functions for to :mod:`inspect`.
:copyright: Copyright (c) 2015 RadiaSoft, Inc. All Rights Reserved.
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
-from __future__ import absolute_import, division, print_function
-
# Avoid pykern imports so avoid dependency issues for pkconfig
from pykern.pkcollections import PKDict
import importlib
diff --git a/pykern/pkunit.py b/pykern/pkunit.py
index d424b392..b5713a5a 100644
--- a/pykern/pkunit.py
+++ b/pykern/pkunit.py
@@ -4,23 +4,21 @@
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
+# defer importing pkconfig
from pykern import pkcompat
from pykern import pkconst
from pykern import pkinspect
from pykern import pkio
-
-# defer importing pkconfig
-import pykern.pkconst
import contextlib
import importlib
import inspect
import json
import os
import py
+import pykern.pkconst
+import pykern.util
import pytest
-import random
import re
-import socket
import subprocess
import sys
import traceback
@@ -279,31 +277,6 @@ def file_eq(expect_path, *args, **kwargs):
_FileEq(expect_path, *args, **kwargs)
-def unbound_localhost_tcp_port(start=10000, stop=20000):
- """Looks for AF_INET SOCK_STREAM port for which bind succeeds
-
- Args:
- start (int): first port [10000]
- stop (int): one greater than last port (passed to range) [20000]
- Returns:
- int: port is available or raises ValueError
- """
-
- def _check_port(port):
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
- s.bind((LOCALHOST_IP, int(port)))
- return port
-
- for p in random.sample(range(start, stop), 100):
- try:
- return _check_port(p)
- except Exception:
- pass
- raise ValueError(
- f"unable find port random sample range={start}-{stop} tries=100 ip={LOCALHOST_IP}"
- )
-
-
def is_test_run():
"""Running in a test?
@@ -505,6 +478,10 @@ def save_chdir_work(is_pkunit_prefix=False, want_empty=True):
)
+#: DEPRECATED
+unbound_localhost_tcp_port = pykern.util.unbound_localhost_tcp_port
+
+
def work_dir():
"""Returns ephemeral work directory, created if necessary.
diff --git a/pykern/util.py b/pykern/util.py
index 72314d54..e367f771 100644
--- a/pykern/util.py
+++ b/pykern/util.py
@@ -4,11 +4,10 @@
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
-# Root module: Limit imports to avoid dependency issues
+# Root module: avoid global pykern imports
import os.path
import sys
-
_ACCEPTABLE_CONTROL_CODE_RATIO = 0.33
_DEFAULT_ROOT = "run"
_DEV_ONLY_FILES = ("setup.py", "pyproject.toml")
@@ -155,3 +154,30 @@ def random_base62(length=16):
r = random.SystemRandom()
return "".join(r.choice(pkconst.BASE62_CHARS) for x in range(length))
+
+
+def unbound_localhost_tcp_port(start=10000, stop=20000):
+ """Looks for AF_INET SOCK_STREAM port for which bind succeeds
+
+ Args:
+ start (int): first port [10000]
+ stop (int): one greater than last port (passed to range) [20000]
+ Returns:
+ int: port is available or raises ValueError
+ """
+ import random, socket
+ from pykern import pkasyncio, pkconst
+
+ def _check_port(port):
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ s.bind((pkconst.LOCALHOST_IP, int(port)))
+ return port
+
+ for p in random.sample(range(start, stop), 100):
+ try:
+ return _check_port(p)
+ except Exception:
+ pass
+ raise ValueError(
+ f"unable find port random sample range={start}-{stop} tries=100 ip={pkconst.LOCALHOST_IP}"
+ )
diff --git a/tests/pkcollections_test.py b/tests/pkcollections_test.py
index 4a218f45..ddcb1909 100644
--- a/tests/pkcollections_test.py
+++ b/tests/pkcollections_test.py
@@ -1,10 +1,9 @@
-# -*- coding: utf-8 -*-
"""pytest for :mod:`pykern.pkcollections`
:copyright: Copyright (c) 2015 RadiaSoft LLC. All Rights Reserved.
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
-from __future__ import absolute_import, division, print_function
+
import pytest
@@ -87,10 +86,11 @@ def test_dict():
def test_dict_copy():
from pykern.pkcollections import PKDict
import copy
- from pykern.pkunit import pkeq, pkne
+ from pykern.pkunit import pkeq, pkne, pkok
n = PKDict(a=1, b=PKDict(c=3))
m = copy.copy(n)
+ pkok(isinstance(m, PKDict), "not pkdict")
pkne(id(n), id(m))
pkeq(id(n.b), id(n.b))
m = copy.deepcopy(n)
@@ -206,6 +206,35 @@ def test_pkmerge():
pkeq(PKDict(one=PKDict(two=[1, 2], three=PKDict(four=[4], four2=4.2)), five=5), s)
+def test_dict_read_only():
+ from pykern.pkcollections import PKDict, PKDictReadOnlyError
+ from pykern import pkunit, pkdebug
+ import copy
+
+ n = PKDict(a=1, b=PKDict(c=3))
+ n.pkset_read_only()
+ n.b.pkset_read_only()
+ pkunit.pkok(n.pkis_read_only(), "n is writable")
+ pkunit.pkok(n.b.pkis_read_only(), "n.b is writable")
+ m = copy.copy(n)
+ pkunit.pkok(m.pkis_read_only(), "m is writable")
+ pkunit.pkok(m.b.pkis_read_only(), "m.b is writable")
+ m = n.copy()
+ pkunit.pkok(not m.pkis_read_only(), "m is read only")
+ pkunit.pkok(m.b.pkis_read_only(), "m.b is writable")
+ m = copy.deepcopy(m)
+ pkunit.pkok(not m.pkis_read_only(), "m is read only")
+ pkunit.pkok(m.b.pkis_read_only(), "m.b is writable")
+ m = PKDict().pkupdate(n)
+ m.b = n.b.copy()
+ pkunit.pkok(not m.pkis_read_only(), "m is read only")
+ pkunit.pkok(not m.b.pkis_read_only(), "m.b is read only")
+ with pkunit.pkexcept(PKDictReadOnlyError):
+ n.pksetdefault1(foo=1)
+ with pkunit.pkexcept(PKDictReadOnlyError):
+ del n["b"]
+
+
def test_subclass():
from pykern import pkcollections
from pykern.pkunit import pkeq, pkok
From 592d6224222cd758638bf6526a00930c680cc028 Mon Sep 17 00:00:00 2001
From: Rob Nagler <5495179+robnagler@users.noreply.github.com>
Date: Mon, 27 Jan 2025 00:25:06 +0000
Subject: [PATCH 3/6] reverse pkcollections
---
pykern/pkcollections.py | 50 -------------------------------------
tests/pkcollections_test.py | 35 +++-----------------------
2 files changed, 3 insertions(+), 82 deletions(-)
diff --git a/pykern/pkcollections.py b/pykern/pkcollections.py
index 8f4aae3c..7d384433 100644
--- a/pykern/pkcollections.py
+++ b/pykern/pkcollections.py
@@ -14,8 +14,6 @@
import types
import pykern.pkcompat
-_READ_ONLY_ATTR = "_PKDict__read_only"
-
class PKDict(dict):
"""A subclass of dict that allows items to be read/written as attributes.
@@ -48,10 +46,6 @@ class PKDict(dict):
only, not general objects.
"""
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- super().__setattr__(_READ_ONLY_ATTR, False)
-
def __delattr__(self, name):
raise PKDictNameError("{}: you cannot delete attributes", name)
@@ -95,7 +89,6 @@ def pkdel(self, name, default=None):
Returns:
object: value (if exists) or default
"""
- self.__assert_not_read_only()
try:
return self[name]
except KeyError:
@@ -106,14 +99,6 @@ def pkdel(self, name, default=None):
except KeyError:
pass
- def pkis_read_only(self):
- """Tests is read only attribute
-
- Returns:
- True: if attribute is read only
- """
- return getattr(self, _READ_ONLY_ATTR)
-
def pkmerge(self, to_merge, make_copy=True):
"""Add `to_merge` to `self`
@@ -215,7 +200,6 @@ def pknested_set(self, qualifiers, value):
Returns:
object: self
"""
- self.__assert_not_read_only()
q = qualifiers.split(".") if isinstance(qualifiers, str) else list(qualifiers)
d = self
for k in q[:-1]:
@@ -243,7 +227,6 @@ def pksetdefault(self, *args, **kwargs):
Returns:
object: self
"""
- self.__assert_not_read_only()
if args and kwargs:
raise AssertionError("one of args or kwargs must be set, but not both")
if args:
@@ -276,7 +259,6 @@ def pksetdefault1(self, *args, **kwargs):
object: ``self[key]``; either `value` if just set, or preexisting value
"""
- self.__assert_not_read_only()
if args and kwargs:
raise AssertionError("one of args or kwargs must be set, but not both")
if args:
@@ -307,38 +289,12 @@ def pkunchecked_nested_get(self, qualifiers, default=None):
except (KeyError, IndexError, TypeError, ValueError):
return default
- def __assert_not_read_only(self):
- if getattr(self, _READ_ONLY_ATTR):
- raise PKDictReadOnlyError()
-
- def pkset_read_only(self):
- """Makes the current instance readonly
-
- Returns:
- PKDict: self
- """
- if self.pkis_read_only():
- return self
- super().__setattr__(_READ_ONLY_ATTR, True)
- for n in (
- "__delitem__",
- "__setitem__",
- "clear",
- "pop",
- "popitem",
- "setdefault" "update",
- ):
- super().__setattr__(n, self.__assert_not_read_only)
- return self
-
def pkupdate(self, *args, **kwargs):
"""Call `dict.update` and return ``self``."""
- self.__assert_not_read_only()
super(PKDict, self).update(*args, **kwargs)
return self
def __pksetdefault_one(self, key, value):
- self.__assert_not_read_only()
if key not in self:
self[key] = value() if callable(value) else value
return self[key]
@@ -350,12 +306,6 @@ class PKDictNameError(NameError):
pass
-class PKDictReadOnlyError(RuntimeError):
- """Raised when an attempt to write read-only PKDict"""
-
- pass
-
-
def canonicalize(obj):
"""Convert to lists and PKDicts for simpler serialization
diff --git a/tests/pkcollections_test.py b/tests/pkcollections_test.py
index ddcb1909..4a218f45 100644
--- a/tests/pkcollections_test.py
+++ b/tests/pkcollections_test.py
@@ -1,9 +1,10 @@
+# -*- coding: utf-8 -*-
"""pytest for :mod:`pykern.pkcollections`
:copyright: Copyright (c) 2015 RadiaSoft LLC. All Rights Reserved.
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
-
+from __future__ import absolute_import, division, print_function
import pytest
@@ -86,11 +87,10 @@ def test_dict():
def test_dict_copy():
from pykern.pkcollections import PKDict
import copy
- from pykern.pkunit import pkeq, pkne, pkok
+ from pykern.pkunit import pkeq, pkne
n = PKDict(a=1, b=PKDict(c=3))
m = copy.copy(n)
- pkok(isinstance(m, PKDict), "not pkdict")
pkne(id(n), id(m))
pkeq(id(n.b), id(n.b))
m = copy.deepcopy(n)
@@ -206,35 +206,6 @@ def test_pkmerge():
pkeq(PKDict(one=PKDict(two=[1, 2], three=PKDict(four=[4], four2=4.2)), five=5), s)
-def test_dict_read_only():
- from pykern.pkcollections import PKDict, PKDictReadOnlyError
- from pykern import pkunit, pkdebug
- import copy
-
- n = PKDict(a=1, b=PKDict(c=3))
- n.pkset_read_only()
- n.b.pkset_read_only()
- pkunit.pkok(n.pkis_read_only(), "n is writable")
- pkunit.pkok(n.b.pkis_read_only(), "n.b is writable")
- m = copy.copy(n)
- pkunit.pkok(m.pkis_read_only(), "m is writable")
- pkunit.pkok(m.b.pkis_read_only(), "m.b is writable")
- m = n.copy()
- pkunit.pkok(not m.pkis_read_only(), "m is read only")
- pkunit.pkok(m.b.pkis_read_only(), "m.b is writable")
- m = copy.deepcopy(m)
- pkunit.pkok(not m.pkis_read_only(), "m is read only")
- pkunit.pkok(m.b.pkis_read_only(), "m.b is writable")
- m = PKDict().pkupdate(n)
- m.b = n.b.copy()
- pkunit.pkok(not m.pkis_read_only(), "m is read only")
- pkunit.pkok(not m.b.pkis_read_only(), "m.b is read only")
- with pkunit.pkexcept(PKDictReadOnlyError):
- n.pksetdefault1(foo=1)
- with pkunit.pkexcept(PKDictReadOnlyError):
- del n["b"]
-
-
def test_subclass():
from pykern import pkcollections
from pykern.pkunit import pkeq, pkok
From 16c0ca358a9a4d291cdf96be52b3b9e1433b006d Mon Sep 17 00:00:00 2001
From: Rob Nagler <5495179+robnagler@users.noreply.github.com>
Date: Mon, 27 Jan 2025 02:09:24 +0000
Subject: [PATCH 4/6] improve logging
---
pykern/http.py | 4 ++--
pykern/http_unit.py | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/pykern/http.py b/pykern/http.py
index 8dfc5cb8..c721e0e7 100644
--- a/pykern/http.py
+++ b/pykern/http.py
@@ -493,7 +493,7 @@ def _reply(call, obj):
r.call_id = call.call_id
self.handler.write_message(_pack_msg(r), binary=True)
except Exception as e:
- pkdlog("exception={} call={} stack={}", call, e, pkdexc())
+ pkdlog("exception={} call={} stack={}", e, call, pkdexc())
self.destroy()
def _quest_kwargs():
@@ -507,7 +507,7 @@ def _quest_kwargs():
self._log("error", None, "msg unpack error={}", [e])
self.destroy()
return None
- self._log("call", c)
+ self._log("call", c, "api={}", [c.api_name])
if not (a := _api(c)):
return
r = await _call(c, a, c.api_args)
diff --git a/pykern/http_unit.py b/pykern/http_unit.py
index eeee3b24..6cf24b28 100644
--- a/pykern/http_unit.py
+++ b/pykern/http_unit.py
@@ -54,9 +54,9 @@ def _client(self):
Returns:
object: http client, set to ``self.client``
"""
- from pykern import http, pkdebug
+ from pykern import http
- return http.HTTPClient(pkdebug.pkdp(self.http_config.copy()))
+ return http.HTTPClient(self.http_config.copy())
def _client_awaitable(self):
"""How to connect to client
From eedb99a433be3440e8980c8a669dc73453637ae6 Mon Sep 17 00:00:00 2001
From: Rob Nagler <5495179+robnagler@users.noreply.github.com>
Date: Mon, 27 Jan 2025 02:13:07 +0000
Subject: [PATCH 5/6] fmt
---
pykern/pkinspect.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/pykern/pkinspect.py b/pykern/pkinspect.py
index b35e2dd7..77735c1b 100644
--- a/pykern/pkinspect.py
+++ b/pykern/pkinspect.py
@@ -3,6 +3,7 @@
:copyright: Copyright (c) 2015 RadiaSoft, Inc. All Rights Reserved.
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
+
# Avoid pykern imports so avoid dependency issues for pkconfig
from pykern.pkcollections import PKDict
import importlib
From 96caac2707aba888f9d1717ffd5043678403ebae Mon Sep 17 00:00:00 2001
From: Rob Nagler <5495179+robnagler@users.noreply.github.com>
Date: Mon, 27 Jan 2025 02:18:23 +0000
Subject: [PATCH 6/6] xlsx test fix due to .00 vs no ".00" version xlsxwriter
3.2.0 to 3.2.1
---
tests/xlsx_data/1.out/xl/worksheets/sheet1.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/xlsx_data/1.out/xl/worksheets/sheet1.xml b/tests/xlsx_data/1.out/xl/worksheets/sheet1.xml
index e6552e44..705823e2 100644
--- a/tests/xlsx_data/1.out/xl/worksheets/sheet1.xml
+++ b/tests/xlsx_data/1.out/xl/worksheets/sheet1.xml
@@ -1,2 +1,2 @@
-37507.7713.14
2345
ROUND(PRODUCT(100--B3,1),2)135.3435.34ROUND(MOD(B3,1),2)0.34IF(B3<=2,"red","green")green
ROUND(MAX(999),2)999.00ROUND(MAX(111,222),2)222.00ROUND(MIN(333,444),2)333.00ROUND(IF(0,1/0,99),2)99.00
AND(TRUE,1<=2)TRUEOR(FALSE,3>4)FALSENOT(6<=7)FALSE6
78
9
\ No newline at end of file
+375.0007.7713.14
2345
ROUND(PRODUCT(100--B3,1),2)135.3435.34ROUND(MOD(B3,1),2)0.34IF(B3<=2,"red","green")green
ROUND(MAX(999),2)999.00ROUND(MAX(111,222),2)222.00ROUND(MIN(333,444),2)333.00ROUND(IF(0,1/0,99),2)99.00
AND(TRUE,1<=2)TRUEOR(FALSE,3>4)FALSENOT(6<=7)FALSE6
78
9
\ No newline at end of file