3
0
Fork 0
mirror of https://github.com/YosysHQ/yosys synced 2025-10-09 09:21:58 +00:00

pyosys: fix ref-only classes, implicit conversions

+ cleanup
This commit is contained in:
Mohamed Gaber 2025-09-28 05:50:37 +03:00
parent c8404bf86b
commit 80fcce64da
No known key found for this signature in database
7 changed files with 122 additions and 67 deletions

View file

@ -2,7 +2,6 @@ name: Build Wheels for PyPI
# run every Sunday at 10 AM # run every Sunday at 10 AM
on: on:
push: # TODO: REMOVE THIS, DO NOT MERGE TO UPSTREAM THIS IS JUST SO I DON'T HAVE TO MANUALLY RUN THE WORKFLOW WITH EVERY PUSH
workflow_dispatch: workflow_dispatch:
schedule: schedule:
- cron: "0 10 * * 0" - cron: "0 10 * * 0"

View file

@ -1017,12 +1017,12 @@ ifeq ($(ENABLE_LIBYOSYS),1)
if [ -n "$(STRIP)" ]; then $(INSTALL_SUDO) $(STRIP) -S $(DESTDIR)$(LIBDIR)/libyosys.so; fi if [ -n "$(STRIP)" ]; then $(INSTALL_SUDO) $(STRIP) -S $(DESTDIR)$(LIBDIR)/libyosys.so; fi
ifeq ($(ENABLE_PYOSYS),1) ifeq ($(ENABLE_PYOSYS),1)
$(INSTALL_SUDO) mkdir -p $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys $(INSTALL_SUDO) mkdir -p $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys
$(INSTALL_SUDO) cp pyosys/__init__.py $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys/__init__.py
$(INSTALL_SUDO) cp libyosys.so $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys/libyosys.so $(INSTALL_SUDO) cp libyosys.so $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys/libyosys.so
$(INSTALL_SUDO) cp -r share $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys $(INSTALL_SUDO) cp -r share $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys
ifeq ($(ENABLE_ABC),1) ifeq ($(ENABLE_ABC),1)
$(INSTALL_SUDO) cp yosys-abc $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys/yosys-abc $(INSTALL_SUDO) cp yosys-abc $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys/yosys-abc
endif endif
$(INSTALL_SUDO) cp misc/__init__.py $(DESTDIR)$(PYTHON_DESTDIR)/$(subst -,_,$(PROGRAM_PREFIX))pyosys/
endif endif
endif endif
ifeq ($(ENABLE_PLUGINS),1) ifeq ($(ENABLE_PLUGINS),1)

View file

@ -198,6 +198,15 @@ bool already_shutdown = false;
PYBIND11_MODULE(pyosys, m) { PYBIND11_MODULE(pyosys, m) {
m.add_object("__path__", py::list()); m.add_object("__path__", py::list());
} }
// Catch uses of 'import libyosys' which can import libyosys.so, causing a ton
// of symbol collisions and overall weird behavior.
//
// This should not affect using wheels as the dylib has to actually be called
// libyosys_dummy.so for this function to be interacted with at all.
PYBIND11_MODULE(libyosys_dummy, _) {
throw py::import_error("Change your import from 'import libyosys' to 'from pyosys import libyosys'.");
}
#endif #endif
void yosys_setup() void yosys_setup()
@ -217,6 +226,8 @@ void yosys_setup()
PyImport_AppendInittab((char*)"pyosys.libyosys", PyInit_libyosys); PyImport_AppendInittab((char*)"pyosys.libyosys", PyInit_libyosys);
// compatibility with wheels // compatibility with wheels
PyImport_AppendInittab((char*)"pyosys", PyInit_pyosys); PyImport_AppendInittab((char*)"pyosys", PyInit_pyosys);
// prevent catastrophes
PyImport_AppendInittab((char*)"libyosys", PyInit_libyosys_dummy);
Py_Initialize(); Py_Initialize();
PyRun_SimpleString("import sys"); PyRun_SimpleString("import sys");
signal(SIGINT, SIG_DFL); signal(SIGINT, SIG_DFL);

View file

@ -33,13 +33,13 @@ is a common problem. ``ruff check pyosys/generator.py`` suffices.
import os import os
import io import io
import shutil import shutil
import argparse
from pathlib import Path from pathlib import Path
from sysconfig import get_paths from sysconfig import get_paths
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, Dict, FrozenSet, Iterable, Tuple, Union, Optional, List from typing import Any, Dict, FrozenSet, Iterable, Tuple, Union, Optional, List
import pybind11 import pybind11
import argparse
from cxxheaderparser.simple import parse_file, ClassScope, NamespaceScope, EnumDecl from cxxheaderparser.simple import parse_file, ClassScope, NamespaceScope, EnumDecl
from cxxheaderparser.options import ParserOptions from cxxheaderparser.options import ParserOptions
from cxxheaderparser.preprocessor import make_gcc_preprocessor from cxxheaderparser.preprocessor import make_gcc_preprocessor
@ -84,6 +84,7 @@ class PyosysClass:
:param denylist: If specified, one or more methods can be excluded from :param denylist: If specified, one or more methods can be excluded from
wrapping. wrapping.
""" """
name: str name: str
ref_only: bool = False ref_only: bool = False
@ -101,6 +102,7 @@ class PyosysHeader:
:param classes: A list of ``PyosysClass`` classes to be wrapped :param classes: A list of ``PyosysClass`` classes to be wrapped
:param enums: A list of enums to be wrapped :param enums: A list of enums to be wrapped
""" """
name: str name: str
classes: List[PyosysClass] = field(default_factory=lambda: []) classes: List[PyosysClass] = field(default_factory=lambda: [])
@ -110,11 +112,13 @@ class PyosysHeader:
for cls in classes: for cls in classes:
self.classes_by_name[cls.name] = cls self.classes_by_name[cls.name] = cls
# MARK: Inclusion and Exclusion # MARK: Inclusion and Exclusion
global_denylist = frozenset( global_denylist = frozenset(
{ {
# deprecated # deprecated
"builtin_ff_cell_types", "builtin_ff_cell_types",
"logv_file_error",
# no implementation # no implementation
"set_verific_logging", "set_verific_logging",
# can't bridge to python cleanly # can't bridge to python cleanly
@ -167,7 +171,11 @@ pyosys_headers = [
} }
), ),
), ),
PyosysClass("Const", string_expr="s.as_string()", denylist=frozenset({"bits", "bitvectorize"})), PyosysClass(
"Const",
string_expr="s.as_string()",
denylist=frozenset({"bits", "bitvectorize"}),
),
PyosysClass("AttrObject", denylist=frozenset({"get_blackbox_attribute"})), PyosysClass("AttrObject", denylist=frozenset({"get_blackbox_attribute"})),
PyosysClass("NamedObject", denylist=frozenset({"get_blackbox_attribute"})), PyosysClass("NamedObject", denylist=frozenset({"get_blackbox_attribute"})),
PyosysClass("Selection"), PyosysClass("Selection"),
@ -211,7 +219,6 @@ pyosys_headers = [
), ),
PyosysClass( PyosysClass(
"Design", "Design",
ref_only=True,
string_expr="s.hashidx_", string_expr="s.hashidx_",
hash_expr="s", hash_expr="s",
denylist=frozenset({"selected_whole_modules"}), # deprecated denylist=frozenset({"selected_whole_modules"}), # deprecated
@ -227,6 +234,7 @@ class PyosysType:
Bit of a hacky object all-around: this is more or less used to encapsulate Bit of a hacky object all-around: this is more or less used to encapsulate
container types so they can be later made opaque using pybind. container types so they can be later made opaque using pybind.
""" """
base: str base: str
specialization: Tuple["PyosysType", ...] specialization: Tuple["PyosysType", ...]
const: bool = False const: bool = False
@ -340,6 +348,10 @@ class PyosysWrapperGenerator(object):
f'\tpy::hashlib::bind_{container.base}<{", ".join(tpl_args)}>(m, "{container.generate_identifier()}");', f'\tpy::hashlib::bind_{container.base}<{", ".join(tpl_args)}>(m, "{container.generate_identifier()}");',
file=self.f_inc, file=self.f_inc,
) )
print(
f"\tpy::implicitly_convertible<py::iterable, {identifier}>();",
file=self.f_inc,
)
print(f"}}", file=self.f_inc) print(f"}}", file=self.f_inc)
# helpers # helpers
@ -414,14 +426,21 @@ class PyosysWrapperGenerator(object):
self.found_containers.update(self.find_containers(supported, target.type)) self.found_containers.update(self.find_containers(supported, target.type))
# processors # processors
def get_overload_cast(self, function: Function, class_basename: Optional[str]) -> str: def get_overload_cast(
self, function: Function, class_basename: Optional[str]
) -> str:
is_method = isinstance(function, Method) is_method = isinstance(function, Method)
function_return_type = function.return_type.format() function_return_type = function.return_type.format()
if class_basename == "Const" and function_return_type in {"iterator", "const_iterator"}: if class_basename == "Const" and function_return_type in {
"iterator",
"const_iterator",
}:
# HACK: qualify Const's iterators # HACK: qualify Const's iterators
function_return_type = f"{class_basename}::{function_return_type}" function_return_type = f"{class_basename}::{function_return_type}"
pointer_kind = f"{class_basename}::*" if (is_method and not function.static) else "*" pointer_kind = (
f"{class_basename}::*" if (is_method and not function.static) else "*"
)
retval = f"static_cast <" retval = f"static_cast <"
retval += function_return_type retval += function_return_type
@ -437,10 +456,17 @@ class PyosysWrapperGenerator(object):
retval += ")" retval += ")"
return retval return retval
def get_definition_args(self, function: Function, class_basename: Optional[str], python_name_override: Optional[str] = None) -> List[str]: def get_definition_args(
self,
function: Function,
class_basename: Optional[str],
python_name_override: Optional[str] = None,
) -> List[str]:
function_basename = function.name.segments[-1].format() function_basename = function.name.segments[-1].format()
python_function_basename = python_name_override or keyword_aliases.get(function_basename, function_basename) python_function_basename = python_name_override or keyword_aliases.get(
function_basename, function_basename
)
def_args = [f'"{python_function_basename}"'] def_args = [f'"{python_function_basename}"']
def_args.append(self.get_overload_cast(function, class_basename)) def_args.append(self.get_overload_cast(function, class_basename))
@ -453,7 +479,7 @@ class PyosysWrapperGenerator(object):
return def_args return def_args
def process_method(self, function: Method, class_basename: str): def process_method(self, metadata: PyosysClass, function: Method):
if ( if (
function.deleted function.deleted
or function.template or function.template
@ -473,6 +499,9 @@ class PyosysWrapperGenerator(object):
return return
if function.constructor: if function.constructor:
if (
not metadata.ref_only
): # ref-only classes should not be constructed from python
print( print(
f"\t\t\t.def(py::init<{self.get_parameter_types(function)}>())", f"\t\t\t.def(py::init<{self.get_parameter_types(function)}>())",
file=self.f, file=self.f,
@ -496,15 +525,13 @@ class PyosysWrapperGenerator(object):
if function.static: if function.static:
definition_fn = "def_static" definition_fn = "def_static"
print(f"\t\t\t.{definition_fn}({', '.join(self.get_definition_args(function, class_basename, python_name_override))})", file=self.f) print(
f"\t\t\t.{definition_fn}({', '.join(self.get_definition_args(function, metadata.name, python_name_override))})",
file=self.f,
)
def process_function(self, function: Function): def process_function(self, function: Function):
if ( if function.deleted or function.template or function.vararg or function.static:
function.deleted
or function.template
or function.vararg
or function.static
):
return return
if function.operator is not None: if function.operator is not None:
@ -516,9 +543,12 @@ class PyosysWrapperGenerator(object):
self.register_containers(function) self.register_containers(function)
print(f"\t\t\tm.def({', '.join(self.get_definition_args(function, None))});", file=self.f) print(
f"\t\t\tm.def({', '.join(self.get_definition_args(function, None))});",
file=self.f,
)
def process_field(self, field: Field, class_basename: str): def process_field(self, metadata: PyosysClass, field: Field):
if field.access != "public": if field.access != "public":
return return
@ -544,7 +574,7 @@ class PyosysWrapperGenerator(object):
field_python_basename = keyword_aliases.get(field.name, field.name) field_python_basename = keyword_aliases.get(field.name, field.name)
print( print(
f'\t\t\t.{definition_fn}("{field_python_basename}", &{class_basename}::{field.name})', f'\t\t\t.{definition_fn}("{field_python_basename}", &{metadata.name}::{field.name})',
file=self.f, file=self.f,
) )
@ -558,9 +588,13 @@ class PyosysWrapperGenerator(object):
self.register_containers(variable) self.register_containers(variable)
definition_fn = f"def_{'readonly' if variable.type.const else 'readwrite'}_static" definition_fn = (
f"def_{'readonly' if variable.type.const else 'readwrite'}_static"
)
variable_python_basename = keyword_aliases.get(variable_basename, variable_basename) variable_python_basename = keyword_aliases.get(
variable_basename, variable_basename
)
variable_name = variable.name.format() variable_name = variable.name.format()
print( print(
@ -569,21 +603,18 @@ class PyosysWrapperGenerator(object):
) )
def process_class_members( def process_class_members(
self, self, metadata: PyosysClass, cls: ClassScope, basename: str
metadata: PyosysClass,
cls: ClassScope,
basename: str
): ):
for method in cls.methods: for method in cls.methods:
if method.name.segments[-1].name in metadata.denylist: if method.name.segments[-1].name in metadata.denylist:
continue continue
self.process_method(method, basename) self.process_method(metadata, method)
visited_anonymous_unions = set() visited_anonymous_unions = set()
for field_ in cls.fields: for field_ in cls.fields:
if field_.name in metadata.denylist: if field_.name in metadata.denylist:
continue continue
self.process_field(field_, basename) self.process_field(metadata, field_)
# Handle anonymous unions # Handle anonymous unions
for subclass in cls.classes: for subclass in cls.classes:
@ -594,7 +625,7 @@ class PyosysWrapperGenerator(object):
continue continue
visited_anonymous_unions.add(au.id) visited_anonymous_unions.add(au.id)
for subfield in subclass.fields: for subfield in subclass.fields:
self.process_field(subfield, basename) self.process_field(metadata, subfield)
def process_class( def process_class(
self, self,
@ -627,10 +658,16 @@ class PyosysWrapperGenerator(object):
self.process_class_members(metadata, base_scope, basename) self.process_class_members(metadata, base_scope, basename)
if expr := metadata.string_expr: if expr := metadata.string_expr:
print(f'\t\t.def("__str__", [](const {basename} &s) {{ return {expr}; }})', file=self.f) print(
f'\t\t.def("__str__", [](const {basename} &s) {{ return {expr}; }})',
file=self.f,
)
if expr := metadata.hash_expr: if expr := metadata.hash_expr:
print(f'\t\t.def("__hash__", [](const {basename} &s) {{ return run_hash({expr}); }})', file=self.f) print(
f'\t\t.def("__hash__", [](const {basename} &s) {{ return run_hash({expr}); }})',
file=self.f,
)
print(f"\t\t;}}", file=self.f) print(f"\t\t;}}", file=self.f)
@ -653,10 +690,12 @@ class PyosysWrapperGenerator(object):
enum_class = enum.typename.classkey == "enum class" enum_class = enum.typename.classkey == "enum class"
for value in enum.values: for value in enum.values:
enum_class_qualifier = f"{basename}::" * enum_class enum_class_qualifier = f"{basename}::" * enum_class
print(f'\t\t\t.value("{value.name}", {enum_class_qualifier}{value.name})', file=self.f) print(
f'\t\t\t.value("{value.name}", {enum_class_qualifier}{value.name})',
file=self.f,
)
print(f"\t\t\t.finalize();}}", file=self.f) print(f"\t\t\t.finalize();}}", file=self.f)
def process_namespace( def process_namespace(
self, self,
header: PyosysHeader, header: PyosysHeader,
@ -668,7 +707,10 @@ class PyosysWrapperGenerator(object):
if len(namespace_components) and (len(ns.functions) + len(ns.variables)): if len(namespace_components) and (len(ns.functions) + len(ns.variables)):
# TODO: Not essential but maybe move namespace usage into # TODO: Not essential but maybe move namespace usage into
# process_function for consistency? # process_function for consistency?
print(f"\t\t{{ using namespace {'::'.join(namespace_components)};", file=self.f) print(
f"\t\t{{ using namespace {'::'.join(namespace_components)};",
file=self.f,
)
for function in ns.functions: for function in ns.functions:
self.process_function(function) self.process_function(function)
for variable in ns.variables: for variable in ns.variables:

View file

@ -95,8 +95,8 @@ void difference(C &lhs, const iterable &rhs) {
template <typename C, typename T> template <typename C, typename T>
void intersect(C &lhs, const iterable &rhs) { void intersect(C &lhs, const iterable &rhs) {
// Doing it in-place is a lot slower // Doing it in-place is a lot slower
// TODO?: Leave modifying lhs to caller (saves a copy) but complicates // TODO?: Leave modifying lhs to caller (saves a copy in some cases)
// chaining intersections. // but complicates chaining intersections.
C storage(lhs); C storage(lhs);
for (auto &element_cxx: lhs) { for (auto &element_cxx: lhs) {
@ -449,6 +449,13 @@ void bind_idict(module &m, const char *name_cstr) {
auto cls = class_<C>(m, name_cstr) auto cls = class_<C>(m, name_cstr)
.def(init<>()) .def(init<>())
.def(init<const C &>()) // copy constructor .def(init<const C &>()) // copy constructor
.def(init([](const iterable &other){ // copy instructor from arbitrary iterables
auto s = new C();
for (auto &e: other) {
(*s)(cast<K>(e));
}
return s;
}))
.def("__len__", [](const C &s){ return (size_t)s.size(); }) .def("__len__", [](const C &s){ return (size_t)s.size(); })
.def("__getitem__", [](const C &s, int v) { return s[v]; }) .def("__getitem__", [](const C &s, int v) { return s[v]; })
.def("__call__", [](C &s, const K &k) { return s(k); }) .def("__call__", [](C &s, const K &k) { return s(k); })
@ -480,20 +487,20 @@ void bind_idict(module &m, const char *name_cstr) {
.def("items", [](args _){ .def("items", [](args _){
throw type_error("idicts do not support pairwise iteration"); throw type_error("idicts do not support pairwise iteration");
}) })
.def("update", [](C &s, iterable iterable) { .def("update", [](C &s, iterable other) {
for (auto &e: iterable) { for (auto &e: other) {
s(cast<K>(e)); s(cast<K>(e));
} }
}) })
.def("__or__", [](const C &s, iterable iterable) { .def("__or__", [](const C &s, iterable other) {
auto result = new C(s); auto result = new C(s);
for (auto &e: iterable) { for (auto &e: other) {
(*result)(cast<K>(e)); (*result)(cast<K>(e));
} }
return result; return result;
}) })
.def("__ior__", [](C &s, iterable iterable) { .def("__ior__", [](C &s, iterable other) {
for (auto &e: iterable) { for (auto &e: other) {
s(cast<K>(e)); s(cast<K>(e));
} }
return s; return s;

View file

@ -21,7 +21,6 @@
// <!-- generated includes --> // <!-- generated includes -->
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
#include <pybind11/native_enum.h> #include <pybind11/native_enum.h>
#include "pyosys/hashlib.h" #include "pyosys/hashlib.h"
namespace py = pybind11; namespace py = pybind11;
@ -35,7 +34,7 @@ using namespace RTLIL;
#include "wrappers.inc.cc" #include "wrappers.inc.cc"
namespace YOSYS_PYTHON { namespace pyosys {
struct Globals {}; struct Globals {};
// Trampolines for Classes with Python-Overridable Virtual Methods // Trampolines for Classes with Python-Overridable Virtual Methods
@ -160,12 +159,6 @@ namespace YOSYS_PYTHON {
} }
}; };
/// @brief Use an auxiliary function to adapt the legacy function.
void log_to_stream(py::object object)
{
// TODO
};
PYBIND11_MODULE(libyosys, m) { PYBIND11_MODULE(libyosys, m) {
// this code is run on import // this code is run on import
m.doc() = "python access to libyosys"; m.doc() = "python access to libyosys";
@ -195,9 +188,9 @@ namespace YOSYS_PYTHON {
auto global_variables = py::class_<Globals>(m, "Globals"); auto global_variables = py::class_<Globals>(m, "Globals");
// Trampoline Classes // Trampoline Classes
py::class_<Pass, YOSYS_PYTHON::PassTrampoline, std::unique_ptr<Pass, py::nodelete>>(m, "Pass") py::class_<Pass, pyosys::PassTrampoline, std::unique_ptr<Pass, py::nodelete>>(m, "Pass")
.def(py::init([](std::string name, std::string short_help) { .def(py::init([](std::string name, std::string short_help) {
auto created = new YOSYS_PYTHON::PassTrampoline(name, short_help); auto created = new pyosys::PassTrampoline(name, short_help);
Pass::init_register(); Pass::init_register();
return created; return created;
}), py::arg("name"), py::arg("short_help")) }), py::arg("name"), py::arg("short_help"))
@ -219,9 +212,9 @@ namespace YOSYS_PYTHON {
.def("call", py::overload_cast<RTLIL::Design *,std::vector<std::string>>(&Pass::call)) .def("call", py::overload_cast<RTLIL::Design *,std::vector<std::string>>(&Pass::call))
; ;
py::class_<RTLIL::Monitor, YOSYS_PYTHON::MonitorTrampoline>(m, "Monitor") py::class_<RTLIL::Monitor, pyosys::MonitorTrampoline>(m, "Monitor")
.def(py::init([]() { .def(py::init([]() {
return new YOSYS_PYTHON::MonitorTrampoline(); return new pyosys::MonitorTrampoline();
})) }))
.def("notify_module_add", &RTLIL::Monitor::notify_module_add) .def("notify_module_add", &RTLIL::Monitor::notify_module_add)
.def("notify_module_del", &RTLIL::Monitor::notify_module_del) .def("notify_module_del", &RTLIL::Monitor::notify_module_del)
@ -255,6 +248,9 @@ namespace YOSYS_PYTHON {
bind_autogenerated_opaque_containers(m); bind_autogenerated_opaque_containers(m);
// <!-- generated pymod-level code --> // <!-- generated pymod-level code -->
py::implicitly_convertible<std::string, RTLIL::IdString>();
py::implicitly_convertible<const char *, RTLIL::IdString>();
}; };
}; };

View file

@ -1,11 +1,11 @@
from pyosys import libyosys as ys from pyosys import libyosys as ys
my_idict = ys.IdstringIdict() my_idict = ys.IdstringIdict()
print(my_idict(ys.IdString("\\hello"))) print(my_idict(ys.IdString("\\hello"))) # test explicit IdString construction
print(my_idict(ys.IdString("\\world"))) print(my_idict("\\world"))
print(my_idict.get(ys.IdString("\\world"))) print(my_idict.get("\\world"))
try: try:
print(my_idict.get(ys.IdString("\\dummy"))) print(my_idict.get("\\dummy"))
except IndexError as e: except IndexError as e:
print(f"{repr(e)}") print(f"{repr(e)}")
print(my_idict[0]) print(my_idict[0])
@ -22,10 +22,10 @@ current_len = len(my_idict)
assert current_len == 2, "copy" assert current_len == 2, "copy"
my_copy = my_idict.copy() my_copy = my_idict.copy()
my_copy(ys.IdString("\\copy")) my_copy("\\copy")
assert len(my_idict) == current_len, "copy seemed to have mutate original idict" assert len(my_idict) == current_len, "copy seemed to have mutate original idict"
assert len(my_copy) == current_len + 1, "copy not behaving as expected" assert len(my_copy) == current_len + 1, "copy not behaving as expected"
current_copy_len = len(my_copy) current_copy_len = len(my_copy)
my_copy |= (ys.IdString(e) for e in ("\\the", "\\world")) # 1 new element my_copy |= ("\\the", "\\world") # 1 new element
assert len(my_copy) == current_copy_len + 1, "or operator returned unexpected result" assert len(my_copy) == current_copy_len + 1, "or operator returned unexpected result"