mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
[Automated] Merged develop into main
This commit is contained in:
commit
519fe2b4d0
12 changed files with 311 additions and 57 deletions
|
|
@ -5,6 +5,7 @@ import maya.mel as mel
|
|||
import six
|
||||
import sys
|
||||
|
||||
from openpype.lib import Logger
|
||||
from openpype.api import (
|
||||
get_project_settings,
|
||||
get_current_project_settings
|
||||
|
|
@ -38,6 +39,8 @@ class RenderSettings(object):
|
|||
"underscore": "_"
|
||||
}
|
||||
|
||||
log = Logger.get_logger("RenderSettings")
|
||||
|
||||
@classmethod
|
||||
def get_image_prefix_attr(cls, renderer):
|
||||
return cls._image_prefix_nodes[renderer]
|
||||
|
|
@ -133,20 +136,7 @@ class RenderSettings(object):
|
|||
|
||||
cmds.setAttr(
|
||||
"defaultArnoldDriver.mergeAOVs", multi_exr)
|
||||
# Passes additional options in from the schema as a list
|
||||
# but converts it to a dictionary because ftrack doesn't
|
||||
# allow fullstops in custom attributes. Then checks for
|
||||
# type of MtoA attribute passed to adjust the `setAttr`
|
||||
# command accordingly.
|
||||
self._additional_attribs_setter(additional_options)
|
||||
for item in additional_options:
|
||||
attribute, value = item
|
||||
if (cmds.getAttr(str(attribute), type=True)) == "long":
|
||||
cmds.setAttr(str(attribute), int(value))
|
||||
elif (cmds.getAttr(str(attribute), type=True)) == "bool":
|
||||
cmds.setAttr(str(attribute), int(value), type = "Boolean") # noqa
|
||||
elif (cmds.getAttr(str(attribute), type=True)) == "string":
|
||||
cmds.setAttr(str(attribute), str(value), type = "string") # noqa
|
||||
reset_frame_range()
|
||||
|
||||
def _set_redshift_settings(self, width, height):
|
||||
|
|
@ -230,12 +220,20 @@ class RenderSettings(object):
|
|||
cmds.setAttr("defaultRenderGlobals.extensionPadding", 4)
|
||||
|
||||
def _additional_attribs_setter(self, additional_attribs):
|
||||
print(additional_attribs)
|
||||
for item in additional_attribs:
|
||||
attribute, value = item
|
||||
if (cmds.getAttr(str(attribute), type=True)) == "long":
|
||||
cmds.setAttr(str(attribute), int(value))
|
||||
elif (cmds.getAttr(str(attribute), type=True)) == "bool":
|
||||
cmds.setAttr(str(attribute), int(value)) # noqa
|
||||
elif (cmds.getAttr(str(attribute), type=True)) == "string":
|
||||
cmds.setAttr(str(attribute), str(value), type = "string") # noqa
|
||||
attribute = str(attribute) # ensure str conversion from settings
|
||||
attribute_type = cmds.getAttr(attribute, type=True)
|
||||
if attribute_type in {"long", "bool"}:
|
||||
cmds.setAttr(attribute, int(value))
|
||||
elif attribute_type == "string":
|
||||
cmds.setAttr(attribute, str(value), type="string")
|
||||
elif attribute_type in {"double", "doubleAngle", "doubleLinear"}:
|
||||
cmds.setAttr(attribute, float(value))
|
||||
else:
|
||||
self.log.error(
|
||||
"Attribute {attribute} can not be set due to unsupported "
|
||||
"type: {attribute_type}".format(
|
||||
attribute=attribute,
|
||||
attribute_type=attribute_type)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from maya import cmds # noqa
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.lib import source_hash
|
||||
from openpype.lib import source_hash, run_subprocess
|
||||
from openpype.pipeline import legacy_io, publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ def find_paths_by_hash(texture_hash):
|
|||
return legacy_io.distinct(key, {"type": "version"})
|
||||
|
||||
|
||||
def maketx(source, destination, *args):
|
||||
def maketx(source, destination, args, logger):
|
||||
"""Make `.tx` using `maketx` with some default settings.
|
||||
|
||||
The settings are based on default as used in Arnold's
|
||||
|
|
@ -79,7 +79,8 @@ def maketx(source, destination, *args):
|
|||
Args:
|
||||
source (str): Path to source file.
|
||||
destination (str): Writing destination path.
|
||||
*args: Additional arguments for `maketx`.
|
||||
args (list): Additional arguments for `maketx`.
|
||||
logger (logging.Logger): Logger to log messages to.
|
||||
|
||||
Returns:
|
||||
str: Output of `maketx` command.
|
||||
|
|
@ -94,7 +95,7 @@ def maketx(source, destination, *args):
|
|||
"OIIO tool not found in {}".format(maketx_path))
|
||||
raise AssertionError("OIIO tool not found")
|
||||
|
||||
cmd = [
|
||||
subprocess_args = [
|
||||
maketx_path,
|
||||
"-v", # verbose
|
||||
"-u", # update mode
|
||||
|
|
@ -103,27 +104,20 @@ def maketx(source, destination, *args):
|
|||
"--checknan",
|
||||
# use oiio-optimized settings for tile-size, planarconfig, metadata
|
||||
"--oiio",
|
||||
"--filter lanczos3",
|
||||
escape_space(source)
|
||||
"--filter", "lanczos3",
|
||||
source
|
||||
]
|
||||
|
||||
cmd.extend(args)
|
||||
cmd.extend(["-o", escape_space(destination)])
|
||||
subprocess_args.extend(args)
|
||||
subprocess_args.extend(["-o", destination])
|
||||
|
||||
cmd = " ".join(cmd)
|
||||
cmd = " ".join(subprocess_args)
|
||||
logger.debug(cmd)
|
||||
|
||||
CREATE_NO_WINDOW = 0x08000000 # noqa
|
||||
kwargs = dict(args=cmd, stderr=subprocess.STDOUT)
|
||||
|
||||
if sys.platform == "win32":
|
||||
kwargs["creationflags"] = CREATE_NO_WINDOW
|
||||
try:
|
||||
out = subprocess.check_output(**kwargs)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(exc)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
out = run_subprocess(subprocess_args)
|
||||
except Exception:
|
||||
logger.error("Maketx converion failed", exc_info=True)
|
||||
raise
|
||||
|
||||
return out
|
||||
|
|
@ -524,15 +518,17 @@ class ExtractLook(publish.Extractor):
|
|||
if do_maketx and ext != ".tx":
|
||||
# Produce .tx file in staging if source file is not .tx
|
||||
converted = os.path.join(staging, "resources", fname + ".tx")
|
||||
|
||||
additional_args = [
|
||||
"--sattrib",
|
||||
"sourceHash",
|
||||
texture_hash
|
||||
]
|
||||
if linearize:
|
||||
self.log.info("tx: converting sRGB -> linear")
|
||||
colorconvert = "--colorconvert sRGB linear"
|
||||
else:
|
||||
colorconvert = ""
|
||||
additional_args.extend(["--colorconvert", "sRGB", "linear"])
|
||||
|
||||
config_path = get_ocio_config_path("nuke-default")
|
||||
color_config = "--colorconfig {0}".format(config_path)
|
||||
additional_args.extend(["--colorconfig", config_path])
|
||||
# Ensure folder exists
|
||||
if not os.path.exists(os.path.dirname(converted)):
|
||||
os.makedirs(os.path.dirname(converted))
|
||||
|
|
@ -541,12 +537,8 @@ class ExtractLook(publish.Extractor):
|
|||
maketx(
|
||||
filepath,
|
||||
converted,
|
||||
# Include `source-hash` as string metadata
|
||||
"--sattrib",
|
||||
"sourceHash",
|
||||
escape_space(texture_hash),
|
||||
colorconvert,
|
||||
color_config
|
||||
additional_args,
|
||||
self.log
|
||||
)
|
||||
|
||||
return converted, COPY, texture_hash
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
"""Collects published version of workfile and increments it.
|
||||
|
||||
For synchronization of published image and workfile version it is required
|
||||
to store workfile version from workfile file name in context.data["version"].
|
||||
In remote publishing this name is unreliable (artist might not follow naming
|
||||
convention etc.), last published workfile version for particular workfile
|
||||
subset is used instead.
|
||||
|
||||
This plugin runs only in remote publishing (eg. Webpublisher).
|
||||
|
||||
Requires:
|
||||
context.data["assetEntity"]
|
||||
|
||||
Provides:
|
||||
context["version"] - incremented latest published workfile version
|
||||
"""
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.client import get_last_version_by_subset_name
|
||||
|
||||
|
||||
class CollectPublishedVersion(pyblish.api.ContextPlugin):
|
||||
"""Collects published version of workfile and increments it."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.190
|
||||
label = "Collect published version"
|
||||
hosts = ["photoshop"]
|
||||
targets = ["remotepublish"]
|
||||
|
||||
def process(self, context):
|
||||
workfile_subset_name = None
|
||||
for instance in context:
|
||||
if instance.data["family"] == "workfile":
|
||||
workfile_subset_name = instance.data["subset"]
|
||||
break
|
||||
|
||||
if not workfile_subset_name:
|
||||
self.log.warning("No workfile instance found, "
|
||||
"synchronization of version will not work.")
|
||||
return
|
||||
|
||||
project_name = context.data["projectName"]
|
||||
asset_doc = context.data["assetEntity"]
|
||||
asset_id = asset_doc["_id"]
|
||||
|
||||
version_doc = get_last_version_by_subset_name(project_name,
|
||||
workfile_subset_name,
|
||||
asset_id)
|
||||
version_int = 1
|
||||
if version_doc:
|
||||
version_int += int(version_doc["name"])
|
||||
|
||||
self.log.debug(f"Setting {version_int} to context.")
|
||||
context.data["version"] = version_int
|
||||
24
openpype/hosts/photoshop/plugins/publish/collect_version.py
Normal file
24
openpype/hosts/photoshop/plugins/publish/collect_version.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
class CollectVersion(pyblish.api.InstancePlugin):
|
||||
"""Collect version for publishable instances.
|
||||
|
||||
Used to synchronize version from workfile to all publishable instances:
|
||||
- image (manually created or color coded)
|
||||
- review
|
||||
|
||||
Dev comment:
|
||||
Explicit collector created to control this from single place and not from
|
||||
3 different.
|
||||
"""
|
||||
order = pyblish.api.CollectorOrder + 0.200
|
||||
label = 'Collect Version'
|
||||
|
||||
hosts = ["photoshop"]
|
||||
families = ["image", "review"]
|
||||
|
||||
def process(self, instance):
|
||||
workfile_version = instance.context.data["version"]
|
||||
self.log.debug(f"Applying version {workfile_version}")
|
||||
instance.data["version"] = workfile_version
|
||||
|
|
@ -577,8 +577,11 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
|
|||
return
|
||||
|
||||
except OSError as exc:
|
||||
# re-raise exception if different than cross drive path
|
||||
if exc.errno != errno.EXDEV:
|
||||
# re-raise exception if different than
|
||||
# EXDEV - cross drive path
|
||||
# EINVAL - wrong format, must be NTFS
|
||||
self.log.debug("Hardlink failed with errno:'{}'".format(exc.errno))
|
||||
if exc.errno not in [errno.EXDEV, errno.EINVAL]:
|
||||
raise
|
||||
|
||||
shutil.copy(src_path, dst_path)
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ class PypeCommands:
|
|||
(to choose validator for example)
|
||||
"""
|
||||
|
||||
from openpype.hosts.webpublisher.cli_functions import (
|
||||
from openpype.hosts.webpublisher.publish_functions import (
|
||||
cli_publish_from_app
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
"vray_renderer": {
|
||||
"image_prefix": "maya/<scene>/<Layer>/<Layer>",
|
||||
"engine": "1",
|
||||
"image_format": "png",
|
||||
"image_format": "exr",
|
||||
"aov_list": [],
|
||||
"additional_options": []
|
||||
},
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
"image_prefix": "maya/<Scene>/<RenderLayer>/<RenderLayer>",
|
||||
"primary_gi_engine": "0",
|
||||
"secondary_gi_engine": "0",
|
||||
"image_format": "iff",
|
||||
"image_format": "exr",
|
||||
"multilayer_exr": true,
|
||||
"force_combine": true,
|
||||
"aov_list": [],
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
"CollectInstances": {
|
||||
"flatten_subset_template": ""
|
||||
},
|
||||
"CollectVersion": {
|
||||
"enabled": false
|
||||
},
|
||||
"ValidateContainers": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
|
|
|
|||
|
|
@ -131,6 +131,23 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "CollectVersion",
|
||||
"label": "Collect Version",
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Synchronize version for image and review instances by workfile version."
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_publish_plugin",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ publishing plugins.
|
|||
"""
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
import qtawesome
|
||||
|
||||
from openpype.pipeline import (
|
||||
install_host,
|
||||
|
|
@ -20,6 +21,27 @@ from openpype.tools.utils.models import (
|
|||
ProjectSortFilterProxy
|
||||
)
|
||||
|
||||
from openpype.tools.utils import PlaceholderLineEdit
|
||||
import appdirs
|
||||
from openpype.lib import JSONSettingRegistry
|
||||
|
||||
|
||||
class TrayPublisherRegistry(JSONSettingRegistry):
|
||||
"""Class handling OpenPype general settings registry.
|
||||
|
||||
Attributes:
|
||||
vendor (str): Name used for path construction.
|
||||
product (str): Additional name used for path construction.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.vendor = "pypeclub"
|
||||
self.product = "openpype"
|
||||
name = "tray_publisher"
|
||||
path = appdirs.user_data_dir(self.product, self.vendor)
|
||||
super(TrayPublisherRegistry, self).__init__(name, path)
|
||||
|
||||
|
||||
class StandaloneOverlayWidget(QtWidgets.QFrame):
|
||||
project_selected = QtCore.Signal(str)
|
||||
|
|
@ -43,6 +65,7 @@ class StandaloneOverlayWidget(QtWidgets.QFrame):
|
|||
projects_model = ProjectModel(dbcon)
|
||||
projects_proxy = ProjectSortFilterProxy()
|
||||
projects_proxy.setSourceModel(projects_model)
|
||||
projects_proxy.setFilterKeyColumn(0)
|
||||
|
||||
projects_view = QtWidgets.QListView(content_widget)
|
||||
projects_view.setObjectName("ChooseProjectView")
|
||||
|
|
@ -59,10 +82,17 @@ class StandaloneOverlayWidget(QtWidgets.QFrame):
|
|||
btns_layout.addWidget(cancel_btn, 0)
|
||||
btns_layout.addWidget(confirm_btn, 0)
|
||||
|
||||
txt_filter = PlaceholderLineEdit(content_widget)
|
||||
txt_filter.setPlaceholderText("Quick filter projects..")
|
||||
txt_filter.setClearButtonEnabled(True)
|
||||
txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"),
|
||||
QtWidgets.QLineEdit.LeadingPosition)
|
||||
|
||||
content_layout = QtWidgets.QVBoxLayout(content_widget)
|
||||
content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
content_layout.setSpacing(20)
|
||||
content_layout.addWidget(header_label, 0)
|
||||
content_layout.addWidget(txt_filter, 0)
|
||||
content_layout.addWidget(projects_view, 1)
|
||||
content_layout.addLayout(btns_layout, 0)
|
||||
|
||||
|
|
@ -79,17 +109,40 @@ class StandaloneOverlayWidget(QtWidgets.QFrame):
|
|||
projects_view.doubleClicked.connect(self._on_double_click)
|
||||
confirm_btn.clicked.connect(self._on_confirm_click)
|
||||
cancel_btn.clicked.connect(self._on_cancel_click)
|
||||
txt_filter.textChanged.connect(self._on_text_changed)
|
||||
|
||||
self._projects_view = projects_view
|
||||
self._projects_model = projects_model
|
||||
self._projects_proxy = projects_proxy
|
||||
self._cancel_btn = cancel_btn
|
||||
self._confirm_btn = confirm_btn
|
||||
self._txt_filter = txt_filter
|
||||
|
||||
self._publisher_window = publisher_window
|
||||
self._project_name = None
|
||||
|
||||
def showEvent(self, event):
|
||||
self._projects_model.refresh()
|
||||
# Sort projects after refresh
|
||||
self._projects_proxy.sort(0)
|
||||
|
||||
setting_registry = TrayPublisherRegistry()
|
||||
try:
|
||||
project_name = setting_registry.get_item("project_name")
|
||||
except ValueError:
|
||||
project_name = None
|
||||
|
||||
if project_name:
|
||||
index = None
|
||||
src_index = self._projects_model.find_project(project_name)
|
||||
if src_index is not None:
|
||||
index = self._projects_proxy.mapFromSource(src_index)
|
||||
if index:
|
||||
mode = (
|
||||
QtCore.QItemSelectionModel.Select
|
||||
| QtCore.QItemSelectionModel.Rows)
|
||||
self._projects_view.selectionModel().select(index, mode)
|
||||
|
||||
self._cancel_btn.setVisible(self._project_name is not None)
|
||||
super(StandaloneOverlayWidget, self).showEvent(event)
|
||||
|
||||
|
|
@ -102,6 +155,10 @@ class StandaloneOverlayWidget(QtWidgets.QFrame):
|
|||
def _on_cancel_click(self):
|
||||
self._set_project(self._project_name)
|
||||
|
||||
def _on_text_changed(self):
|
||||
self._projects_proxy.setFilterRegularExpression(
|
||||
self._txt_filter.text())
|
||||
|
||||
def set_selected_project(self):
|
||||
index = self._projects_view.currentIndex()
|
||||
|
||||
|
|
@ -119,6 +176,9 @@ class StandaloneOverlayWidget(QtWidgets.QFrame):
|
|||
self.setVisible(False)
|
||||
self.project_selected.emit(project_name)
|
||||
|
||||
setting_registry = TrayPublisherRegistry()
|
||||
setting_registry.set_item("project_name", project_name)
|
||||
|
||||
|
||||
class TrayPublishWindow(PublisherWindow):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -330,11 +330,26 @@ class ProjectModel(QtGui.QStandardItemModel):
|
|||
if new_items:
|
||||
root_item.appendRows(new_items)
|
||||
|
||||
def find_project(self, project_name):
|
||||
"""
|
||||
Get index of 'project_name' value.
|
||||
|
||||
Args:
|
||||
project_name (str):
|
||||
Returns:
|
||||
(QModelIndex)
|
||||
"""
|
||||
val = self._items_by_name.get(project_name)
|
||||
if val:
|
||||
return self.indexFromItem(val)
|
||||
|
||||
|
||||
class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProjectSortFilterProxy, self).__init__(*args, **kwargs)
|
||||
self._filter_enabled = True
|
||||
# Disable case sensitivity
|
||||
self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
def lessThan(self, left_index, right_index):
|
||||
if left_index.data(PROJECT_NAME_ROLE) is None:
|
||||
|
|
@ -356,10 +371,14 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
|||
|
||||
def filterAcceptsRow(self, source_row, source_parent):
|
||||
index = self.sourceModel().index(source_row, 0, source_parent)
|
||||
string_pattern = self.filterRegularExpression().pattern()
|
||||
if self._filter_enabled:
|
||||
result = self._custom_index_filter(index)
|
||||
if result is not None:
|
||||
return result
|
||||
project_name = index.data(PROJECT_NAME_ROLE)
|
||||
if project_name is None:
|
||||
return result
|
||||
return string_pattern.lower() in project_name.lower()
|
||||
|
||||
return super(ProjectSortFilterProxy, self).filterAcceptsRow(
|
||||
source_row, source_parent
|
||||
|
|
|
|||
83
tools/get_python_packages_info.py
Normal file
83
tools/get_python_packages_info.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Get version and license information on used Python packages.
|
||||
|
||||
This is getting over all packages installed with Poetry and printing out
|
||||
their name, version and available license information from PyPi in Markdown
|
||||
table format.
|
||||
|
||||
Usage:
|
||||
./.poetry/bin/poetry run python ./tools/get_python_packages_info.py
|
||||
|
||||
"""
|
||||
|
||||
import toml
|
||||
import requests
|
||||
|
||||
|
||||
packages = []
|
||||
|
||||
# define column headers
|
||||
package_header = "Package"
|
||||
version_header = "Version"
|
||||
license_header = "License"
|
||||
|
||||
name_col_width = len(package_header)
|
||||
version_col_width = len(version_header)
|
||||
license_col_width = len(license_header)
|
||||
|
||||
# read lock file to get packages
|
||||
with open("poetry.lock", "r") as fb:
|
||||
lock_content = toml.load(fb)
|
||||
|
||||
for package in lock_content["package"]:
|
||||
# query pypi for license information
|
||||
url = f"https://pypi.org/pypi/{package['name']}/json"
|
||||
response = requests.get(
|
||||
f"https://pypi.org/pypi/{package['name']}/json")
|
||||
package_data = response.json()
|
||||
version = package.get("version") or "N/A"
|
||||
try:
|
||||
package_license = package_data["info"].get("license") or "N/A"
|
||||
except KeyError:
|
||||
package_license = "N/A"
|
||||
|
||||
if len(package_license) > 64:
|
||||
package_license = f"{package_license[:32]}..."
|
||||
packages.append(
|
||||
(
|
||||
package["name"],
|
||||
version,
|
||||
package_license
|
||||
)
|
||||
)
|
||||
|
||||
# update column width based on max string length
|
||||
if len(package["name"]) > name_col_width:
|
||||
name_col_width = len(package["name"])
|
||||
if len(version) > version_col_width:
|
||||
version_col_width = len(version)
|
||||
if len(package_license) > license_col_width:
|
||||
license_col_width = len(package_license)
|
||||
|
||||
# pad columns
|
||||
name_col_width += 2
|
||||
version_col_width += 2
|
||||
license_col_width += 2
|
||||
|
||||
# print table header
|
||||
print((f"|{package_header.center(name_col_width)}"
|
||||
f"|{version_header.center(version_col_width)}"
|
||||
f"|{license_header.center(license_col_width)}|"))
|
||||
|
||||
print(
|
||||
"|" + ("-" * len(package_header.center(name_col_width))) +
|
||||
"|" + ("-" * len(version_header.center(version_col_width))) +
|
||||
"|" + ("-" * len(license_header.center(license_col_width))) + "|")
|
||||
|
||||
# print rest of the table
|
||||
for package in packages:
|
||||
print((
|
||||
f"|{package[0].center(name_col_width)}"
|
||||
f"|{package[1].center(version_col_width)}"
|
||||
f"|{package[2].center(license_col_width)}|"
|
||||
))
|
||||
Loading…
Add table
Add a link
Reference in a new issue