Source code for skidl.part_query

# -*- coding: utf-8 -*-

# The MIT License (MIT) - Copyright (c) 2016-2021 Dave Vandenbout.

"""
Functions for finding/displaying parts and footprints.
"""

from __future__ import (  # isort:skip
    absolute_import,
    division,
    print_function,
    unicode_literals,
)

import os
import os.path
import re
from builtins import open, super

from future import standard_library

from .logger import active_logger
from .utilities import *

standard_library.install_aliases()

# TODO: Use push-down automata to parse nested parenthetical expression
#       of AND/OR clauses for use in advanced part searching.
#       https://stackoverflow.com/questions/4284991/parsing-nested-parentheses-in-python-grab-content-by-level


[docs]def parse_search_terms(terms): """ Return a regular expression for a sequence of search terms. Substitute a zero-width lookahead assertion (?= ) for each term. Thus, the "abc def" would become "(?=.*(abc))(?=.*(def))" and would match any string containing both "abc" and "def". Or "abc (def|ghi)" would become "(?=.*(abc))((?=.*(def|ghi))" and would match any string containing "abc" and "def" or "ghi". Quoted terms can be used for phrases containing whitespace. """ # Place the quote-delimited REs before the RE for sequences of # non-white chars to prevent the initial portion of a quoted string from being # gathered up as a non-white character sequence. terms = terms.strip().rstrip() # Remove leading/trailing spaces. terms = re.sub(r"\s*\|\s*", r"|", terms) # Remove spaces around OR operator. terms = re.sub(r"((\".*?\")|(\'.*?\')|(\S+))\s*", r"(?=.*(\1))", terms) terms = re.sub(r"[\'\"]", "", terms) # Remove quotes. terms = terms + ".*" return terms
[docs]def search_parts_iter(terms, tool=None): """Return a list of (lib, part) sequences that match a regex term.""" import skidl from .schlib import SchLib if tool is None: tool = skidl.get_default_tool() terms = parse_search_terms(terms) def mk_list(l): """Make a list out of whatever is given.""" if isinstance(l, (list, tuple)): return l if not l: return [] return [l] # Gather all the lib files from all the directories in the search paths. lib_files = list() lib_suffixes = tuple(to_list(skidl.lib_suffixes[tool])) for lib_dir in skidl.lib_search_paths[tool]: # Get all the library files in the search path. try: files = os.listdir(lib_dir) except (FileNotFoundError, OSError): active_logger.warning("Could not open directory '{}'".format(lib_dir)) files = [] files = [(lib_dir, l) for l in files if l.endswith(lib_suffixes)] lib_files.extend(files) num_lib_files = len(lib_files) # Now search through the lib files for parts that match the search terms. for idx, (lib_dir, lib_file) in enumerate(lib_files): # If just entered a new lib file, yield the name of the file and # where it is within the total number of files to search. # (This is used for progress indicators.) yield "LIB", lib_file, idx + 1, num_lib_files # Parse the lib file to create a part library. lib = SchLib( os.path.join(lib_dir, lib_file), tool=tool ) # Open the library file. # Search the current library for parts with the given terms. for part in mk_list( # Get any matching parts from the library file. lib.get_parts(use_backup_lib=False, search_text=terms) ): # Parse the part to instantiate the complete object. part.parse(get_name_only=True) # Yield the part and its containing library. yield "PART", lib_file, part, part.name # Also return aliases. for alias in list(part.aliases): yield "PART", lib_file, part, alias
[docs]def search_parts(terms, tool=None): """ Print a list of parts with the regex terms within their name, alias, description or keywords. """ parts = set() for part in search_parts_iter(terms, tool): if part[0] == "LIB": print(" " * 79, "\rSearching {} ...".format(part[1]), sep="", end="\r") elif part[0] == "PART": parts.add(part[1:4]) print(" " * 79, end="\r") # Print each part name sorted by the library where it was found. for lib_file, part, part_name in sorted(list(parts), key=lambda p: p[0]): print( "{}: {} ({})".format( lib_file, part_name, getattr(part, "description", "???") ) )
[docs]def show_part(lib, part_name, tool=None): """ Print the I/O pins for a given part in a library. Args: lib: Either a SchLib object or the name of a library. part_name: The name of the part in the library. tool: The ECAD tool format for the library. Returns: A Part object. """ import skidl from .part import TEMPLATE, Part if tool is None: tool = skidl.get_default_tool() try: return Part(lib, re.escape(part_name), tool=tool, dest=TEMPLATE) except Exception: return None
[docs]class FootprintCache(dict): """Dict for storing footprints from all directories.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.reset() # Cache starts off empty, hence invalid.
[docs] def reset(self): self.clear() # Clear out cache. self.valid = False # Cache is empty, hence invalid.
[docs] def load(self, path): """Load cache with footprints from libraries in fp-lib-table file.""" # Expand any env. vars and/or user in the path. path = os.path.expandvars(os.path.expanduser(path)) # Read contents of footprint library file into a single string. try: # Look for fp-lib-table file and read its entries into a table of footprint module libs. with open(os.path.join(path, "fp-lib-table")) as fp: tbl = fp.read() except FileNotFoundError: # fp-lib-table file was not found, so create a table containing the path directory # as a single module lib. nickname, ext = os.path.splitext(os.path.basename(path)) tbl = '(fp_lib_table\n(lib (name {nickname})(type KiCad)(uri {path})(options "")(descr ""))\n)'.format( **locals() ) # Get individual "(lib ...)" entries from the string. libs = re.findall( r"\(\s*lib\s* .*? \)\)", tbl, flags=re.IGNORECASE | re.VERBOSE | re.DOTALL ) # Add the footprint modules found in each enabled KiCad library. for lib in libs: # Skip disabled libraries. disabled = re.findall( r"\(\s*disabled\s*\)", lib, flags=re.IGNORECASE | re.VERBOSE ) if disabled: continue # Skip non-KiCad libraries (primarily git repos). type_ = re.findall( r'(?:\(\s*type\s*) ("[^"]*?"|[^)]*?) (?:\s*\))', lib, flags=re.IGNORECASE | re.VERBOSE, )[0] if type_.lower() != "kicad": continue # Get the library directory and nickname. uri = re.findall( r'(?:\(\s*uri\s*) ("[^"]*?"|[^)]*?) (?:\s*\))', lib, flags=re.IGNORECASE | re.VERBOSE, )[0] nickname = re.findall( r'(?:\(\s*name\s*) ("[^"]*?"|[^)]*?) (?:\s*\))', lib, flags=re.IGNORECASE | re.VERBOSE, )[0] # Remove any quotes around the URI or nickname. uri = rmv_quotes(uri) nickname = rmv_quotes(nickname) # Expand environment variables and ~ in the URI. uri = os.path.expandvars(os.path.expanduser(uri)) # Look for unexpanded env vars and skip this loop iteration if found. def get_env_vars(s): """Return a list of environment variables found in a string.""" env_vars = [] for env_var_re in (r"\${([^}]*)}", r"\$(\w+)", r"%(\w+)%"): env_vars.extend(re.findall(env_var_re, s)) return env_vars unexpanded_vars = get_env_vars(uri) if unexpanded_vars: active_logger.warning( "There are some undefined environment variables: {}".format( " ".join(unexpanded_vars) ) ) continue # Get a list of all the footprint module files in the top-level of the library URI. filenames = [ fn for fn in os.listdir(uri) if os.path.isfile(os.path.join(uri, fn)) and fn.lower().endswith(".kicad_mod") ] # Create an entry in the cache for this nickname. (This will overwrite # any previous nickname entry, so make sure to scan fp-lib-tables in order of # increasing priority.) Each entry contains the path to the directory containing # the footprint module and a dictionary of the modules keyed by the module name # with an associated value containing the module file contents (which starts off # as None). self[nickname] = { "path": uri, "modules": {os.path.splitext(fn)[0]: None for fn in filenames}, }
# Cache for storing footprints read from .kicad_mod files. footprint_cache = FootprintCache()
[docs]def search_footprints_iter(terms, tool=None): """Return a list of (lib, footprint) sequences that match a regex term.""" import skidl if tool is None: tool = skidl.get_default_tool() terms = parse_search_terms(terms) # If the cache isn't valid, then make it valid by gathering all the # footprint files from all the directories in the search paths. if not footprint_cache.valid: footprint_cache.clear() for path in skidl.footprint_search_paths[tool]: footprint_cache.load(path) # Get the number of footprint libraries to be searched.. num_fp_libs = len(footprint_cache) # Now search through the libraries for footprints that match the search terms. for idx, fp_lib in enumerate(footprint_cache): # If just entered a new library, yield the name of the lib and # where it is within the total number of libs to search. # (This is used for progress indicators.) yield "LIB", fp_lib, idx + 1, num_fp_libs # Get path to library directory and dict of footprint modules. path = footprint_cache[fp_lib]["path"] modules = footprint_cache[fp_lib]["modules"] # Search each module in the library. for module_name in modules: # If the cache isn't valid, then read each footprint file and store # it's contents in the cache. if not footprint_cache.valid: file = os.path.join(path, module_name + ".kicad_mod") with open(file, "r") as fp: try: # Remove any linefeeds that would interfere with fullmatch() later on. modules[module_name] = [l.rstrip() for l in fp.readlines()] except UnicodeDecodeError: try: modules[module_name] = [ l.decode("utf-8").rstrip() for l in fp.readlines() ] except AttributeError: modules[module_name] = "" # Get the contents of the footprint file from the cache. module_text = tuple(modules[module_name]) # Count the pads so it can be added to the text being searched. # Join all the module text lines, search for the number of # occurrences of "(pad", and then count them. # A set is used so pads with the same num/name are only counted once. # Place the pad count before everything else so the space that # terminates it won't be stripped off later. This is necessary # so (for example) "#pads=20 " won't match "#pads=208". num_pads = len( set(re.findall(r"\(\s*pad\s+([^\s)]+)", " ".join(module_text))) ) num_pads_str = "#pads={}".format(num_pads) # Create a string with the module name, library name, number of pads, # description and tags. search_text = "\n".join([num_pads_str, fp_lib, module_name]) for line in module_text: if "(descr " in line or "(tags " in line: search_text = "\n".join([search_text, line]) # Search the string for a match with the search terms. if fullmatch( terms, search_text, flags=re.IGNORECASE | re.MULTILINE | re.DOTALL ): yield "MODULE", fp_lib, module_text, module_name # At the end, all modules have been scanned and the footprint cache is valid. footprint_cache.valid = True
[docs]def search_footprints(terms, tool=None): """ Print a list of footprints with the regex term within their description/tags. """ footprints = [] for fp in search_footprints_iter(terms, tool): if fp[0] == "LIB": print(" " * 79, "\rSearching {} ...".format(fp[1]), sep="", end="\r") elif fp[0] == "MODULE": footprints.append(fp[1:4]) print(" " * 79, end="\r") # Print each module name sorted by the library where it was found. for lib_file, module_text, module_name in sorted( footprints, key=lambda f: (f[0], f[2]) ): descr = "???" tags = "???" for line in module_text: try: descr = line.split("(descr ")[1].rsplit(")", 1)[0] except IndexError: pass try: tags = line.split("(tags ")[1].rsplit(")", 1)[0] except IndexError: pass print("{}: {} ({} - {})".format(lib_file, module_name, descr, tags))
[docs]def show_footprint(lib, module_name, tool=None): """ Print the pads for a given module in a library. Args: lib: The name of a library. module_name: The name of the footprint in the library. tool: The ECAD tool format for the library. Returns: A Part object. """ import skidl if tool is None: tool = skidl.get_default_tool() os.environ["KISYSMOD"] = os.pathsep.join(skidl.footprint_search_paths[tool]) return pym.Module.from_library(lib, module_name)
# Define some shortcuts. search = search_parts show = show_part