# -*- coding: utf-8 -*-
# The MIT License (MIT) - Copyright (c) Dave Vandenbout.
"""
Handles schematic libraries for various ECAD tools.
"""
import re
from .alias import Alias
from .logger import active_logger
from .utilities import (
consistent_hash,
cnvt_to_var_name,
export_to_all,
filter_list,
flatten,
is_url,
list_or_scalar,
opened,
get_abs_filename,
norecurse,
)
[docs]
@export_to_all
class SchLib(object):
"""
A class for storing parts from a schematic component library file.
Attributes:
filename: The name of the file from which the parts were read.
parts: The list of parts (composed of Part objects).
Args:
filename: The name of the library file.
tool: The format of the library file (e.g., KICAD).
lib_section: The section of the library to access (for SPICE, only).
use_cache: If true, use a cache of libraries to speed up loading.
use_pickle: If true, pickle the library for faster loading next time.
Keyword Args:
attribs: Key/value pairs of attributes to add to the library.
"""
# Keep a dict of filenames and their associated SchLib object
# for fast loading of libraries.
_cache = {}
def __init__(
self,
filename=None,
tool=None,
lib_section=None,
use_cache=True,
use_pickle=True,
**attribs
):
"""
Load the parts from a library file.
"""
import os
import pickle
import skidl
from .tools import tool_modules, lib_suffixes
tool = tool or skidl.config.tool
# Library starts off empty of parts.
self.parts = []
# Attach attributes to the library.
for k, v in list(attribs.items()):
setattr(self, k, v)
# If no filename, just create an empty library and exit.
if not filename:
return
# Get the absolute path for the part library file.
try:
paths = skidl.lib_search_paths[tool]
exts = lib_suffixes[tool]
except KeyError:
# OK, unknown tool...
active_logger.raise_(
ValueError,
"Unsupported ECAD tool library: {}.".format(tool),
)
abs_filename = get_abs_filename(
filename, paths, exts, allow_failure=False, descend=-1
)
# Don't pickle files stored in remote repos because it's difficult to
# get their modification times to compare against the local pickled library
# to see which is fresher. So remote libs are never pickled.
# TODO: Allow pickling of remote part libraries.
use_pickle = use_pickle and not is_url(abs_filename)
# Get a unique hash to reference the part library file.
abs_fn_hash = consistent_hash(abs_filename)
# Create the absolute file name of the pickle file for storing this part library.
# The pickle file name is based on the library name, the tool name, and the hash.
# It is stored in a directory specified in the SKIDL configuration file.
lib_name, lib_ext = os.path.splitext(os.path.split(abs_filename)[1])
lib_pickle_abs_fn = os.path.abspath(os.path.join(
skidl.config.pickle_dir, "_".join((lib_name, tool, str(abs_fn_hash)))
) + ".pkl")
# Load this SchLib with an existing SchLib object if the file name hash
# matches one in the cache.
if lib_pickle_abs_fn in self._cache:
self.__dict__.update(self._cache[lib_pickle_abs_fn].__dict__)
# Load this Schlib from the pickle file if it exists and it's more recent
# than the original part library file.
elif (
use_pickle
and os.path.exists(lib_pickle_abs_fn)
and os.path.getmtime(lib_pickle_abs_fn) >= os.path.getmtime(abs_filename)
):
with open(lib_pickle_abs_fn, "rb") as f:
self.__dict__.update(pickle.load(f).__dict__)
# Cache a reference to the library.
if use_cache:
self._cache[lib_pickle_abs_fn] = self
# Otherwise, load from a schematic part library file.
else:
# Use the tool name to find the function for loading the library.
tool_modules[tool].load_sch_lib(
self,
abs_filename,
# skidl.lib_search_paths[tool],
lib_section=lib_section,
)
self.filename = filename
# Cache a reference to the library.
if use_cache:
self._cache[lib_pickle_abs_fn] = self
# Pickle the library for future use.
if use_pickle:
if not os.path.exists(skidl.config.pickle_dir):
os.mkdir(skidl.config.pickle_dir)
with open(lib_pickle_abs_fn, "wb") as f:
try:
pickle.dump(self, f)
except Exception as e:
pass
# Delete the pickled lib if its size if zero (i.e., a pickling error occurred).
if os.path.exists(lib_pickle_abs_fn):
if os.path.getsize(lib_pickle_abs_fn) == 0:
# Delete the file
os.remove(lib_pickle_abs_fn)
def __str__(self):
"""Return a list of the part names in this library as a string."""
return "\n".join(["{}: {}".format(p.name, p.description) for p in self.parts])
__repr__ = __str__
def __len__(self):
"""
Return number of parts in library.
"""
return len(self.parts)
def __getitem__(self, id):
"""Get part by name or alias."""
return list_or_scalar(self.get_parts_by_name(id))
def __iadd__(self, *parts):
"""Add one or more parts to a library."""
return self.add_parts(*parts)
[docs]
@classmethod
def reset(cls):
"""Clear the cache of processed library files."""
cls._cache = {}
[docs]
def add_parts(self, *parts):
"""Add one or more parts to a library."""
from .part import TEMPLATE
for part in flatten(parts):
# Parts with the same name are not allowed in the library.
if not self.get_parts_by_name(
part.name, be_thorough=False, allow_failure=True
):
self.parts.append(part.copy(dest=TEMPLATE))
# Place a pointer to this library into the added part.
self.parts[-1].lib = self
return self
[docs]
def get_parts(self, use_backup_lib=True, **criteria):
"""
Return parts from a library that match *all* the given criteria.
Keyword Args:
criteria: One or more keyword-argument pairs. The keyword specifies
the attribute name while the argument contains the desired value
of the attribute.
Returns:
A list of Parts that match all the criteria.
"""
import skidl
parts = filter_list(self.parts, **criteria)
if not parts and use_backup_lib and skidl.config.query_backup_lib:
try:
backup_lib = load_backup_lib()
parts = backup_lib.get_parts(use_backup_lib=False, **criteria)
except AttributeError:
pass
return parts
[docs]
def get_parts_quick(self, name):
"""Do a quick search for a part name or alias."""
return [prt for prt in self.parts if prt.aliases == name]
[docs]
def get_parts_by_name(
self,
name,
be_thorough=True,
allow_multiples=False,
allow_failure=False,
partial_parse=False,
):
"""
Return a Part with the given name or alias from the part list.
Args:
name: The part name or alias to search for in the library.
be_thorough: Do thorough search, not just simple string matching.
allow_multiples: If true, return a list of parts matching the name.
If false, return only the first matching part and issue
a warning if there were more than one.
allow_failure: Return None if no matches found. Issue no errors/warnings.
partial_parse: If true, don't fully parse any parts that are found.
Returns:
A list of Parts that match all the criteria.
"""
# Start with a simple search for the part name.
names = Alias(name, name.lower(), name.upper())
parts = self.get_parts_quick(names)
# Simple search failed, so try the more thorough search method.
if not parts and be_thorough:
parts = self.get_parts(aliases=name)
# No parts found, so signal an error.
if not parts and not allow_failure:
message = "Unable to find part {} in library {}.".format(
name, getattr(self, "filename", "UNKNOWN")
)
active_logger.raise_(ValueError, message)
if len(parts) > 1 and not allow_multiples:
message = "Found multiple parts matching {}. Selecting {}.".format(
name, parts[0].name
)
active_logger.warning(message)
parts = parts[0:1] # Just keep the first part.
# Do whatever parsing was requested for the found parts.
for part in parts:
part.parse(partial_parse)
return parts
[docs]
def export(self, libname, file_=None, tool=None, addtl_part_attrs=None):
"""
Export a library into a file.
Args:
libname: A string containing the name of the library.
file_: The file the library will be exported to. It can either
be a file object or a string or None. If None, the file
will be the same as the library name with the library
suffix appended.
tool: The CAD tool library format to be used. Currently, this can
only be SKIDL.
addtl_part_attrs (list): List of additional part attribute names to include in export.
"""
def prettify(s):
"""Breakup and indent library export string."""
s = re.sub(r"(Part\()", r"\n \1", s)
s = re.sub(r"(Pin\()", r"\n \1", s)
return s
from skidl import SKIDL
from skidl.tools import lib_suffixes
if tool is None:
tool = SKIDL
file_ = file_ or (libname + lib_suffixes[tool])
export_str = "from collections import defaultdict\n"
export_str += "from skidl import Pin, Part, Alias, SchLib, SKIDL, TEMPLATE\n\n"
export_str += "from skidl.pin import pin_types\n\n"
export_str += "SKIDL_lib_version = '0.0.1'\n\n"
part_export_str = ",".join(
[p.export(addtl_part_attrs=addtl_part_attrs) for p in self.parts]
)
export_str += "{} = SchLib(tool=SKIDL).add_parts(*[{}])".format(
cnvt_to_var_name(libname), part_export_str
)
export_str = prettify(export_str)
with opened(file_, "w") as f:
f.write(export_str)
@export_to_all
@norecurse
def load_backup_lib():
"""Load a backup library that stores the parts used in the circuit."""
from . import skidl
# Don't keep reloading the backup library once it's loaded.
if not skidl.config.backup_lib:
try:
# The backup library is a SKiDL lib stored as a Python module.
glb_vars, loc_vars = None, locals()
exec(open(skidl.config.backup_lib_file_name).read(), glb_vars, loc_vars)
# Copy the backup library in the local storage to the global storage.
skidl.config.backup_lib = loc_vars[skidl.config.backup_lib_name]
except (FileNotFoundError, ImportError, NameError, IOError):
pass
return skidl.config.backup_lib