Repository: FHPythonUtils/LicenseCheck

Issue:
Bug: setuptools declares classifier as an alias of classifiers but LicenseCheck fails
## Before You Begin

Before proceeding, please make sure to follow these steps:

- [X] I have checked for similar issues in the project's issue tracker.
- [X] I have searched closed issues to see if a similar problem was reported
  before.

## Issue Details

When running licensecheck, the CLI raises a traceback with error:

```
configparser.NoOptionError: No option 'classifiers' in section: 'metadata'
```

### Description

licensecheck seems to only permit using the attribute `classifiers` and not the alias `classifier`.

However, the official documentation for setuptools lists classifier as an alias.

https://setuptools.pypa.io/en/latest/userguide/declarative_config.html#metadata

### Expected Behavior

I'd expect licensecheck to treat classifier the same as classifiers metadata attribute. When changing my setup.cfg metadata from `classifier` to `classifiers` it works without error.

### Actual Behavior

```
Erics-MacBook-Pro:precli ericwb$ licensecheck -f ansi -u requirements:requirements.txt
Traceback (most recent call last):
  File "/Users/ericwb/.pyenv/versions/3.13.0/lib/python3.13/configparser.py", line 818, in get
    value = d[option]
            ~^^^^^^^^
  File "/Users/ericwb/.pyenv/versions/3.13.0/lib/python3.13/collections/__init__.py", line 1016, in __getitem__
    return self.__missing__(key)            # support subclasses that define __missing__
           ~~~~~~~~~~~~~~~~^^^^^
  File "/Users/ericwb/.pyenv/versions/3.13.0/lib/python3.13/collections/__init__.py", line 1008, in __missing__
    raise KeyError(key)
KeyError: 'classifiers'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/ericwb/.pyenv/versions/3.13.0/bin/licensecheck", line 8, in <module>
    sys.exit(cli())
             ~~~^^
  File "/Users/ericwb/.pyenv/versions/3.13.0/lib/python3.13/site-packages/licensecheck/cli.py", line 86, in cli
    sysexit(main(args))
            ~~~~^^^^^^
  File "/Users/ericwb/.pyenv/versions/3.13.0/lib/python3.13/site-packages/licensecheck/cli.py", line 120, in main
    myLiceTxt = args["license"] if args.get("license") else packageinfo.getMyPackageLicense()
                                                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/Users/ericwb/.pyenv/versions/3.13.0/lib/python3.13/site-packages/licensecheck/packageinfo.py", line 184, in getMyPackageLicense
    metaData = getMyPackageMetadata()
  File "/Users/ericwb/.pyenv/versions/3.13.0/lib/python3.13/site-packages/licensecheck/packageinfo.py", line 159, in getMyPackageMetadata
    classifiers = config.get("metadata", "classifiers").strip().splitlines()
                  ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/ericwb/.pyenv/versions/3.13.0/lib/python3.13/configparser.py", line 821, in get
    raise NoOptionError(option, section)
configparser.NoOptionError: No option 'classifiers' in section: 'metadata'
```

## System Information

Please provide the following additional information about your system or
environment:

- Operating System (OS): macOS
- OS Version: Sequoia 15.3
- Browser (if applicable): n/a
- Browser Version (if applicable): n/a
- Project Version (if applicable): n/a

Discussion:
@FredHappyface: Hi thanks so much for the issue, and apologies for the delay in getting around to this one! 

I plan on taking a look at this over the coming weeks! As part of this, I'll be writing some clearer documentation outlining the supported workflows as I completely forget how many distinct workflows the python ecosystem supports! Ideally, I'll get some level of support for this implemented too 

Unfortunately it's hard to give an exact time-frame on this as I'd like to work through as many issues as I can before releasing. The hope is to get this all rolled out by the end of March :) 

Solution patch:
diff --git a/licensecheck/cli.py b/licensecheck/cli.py
index ed711ed..4b6d651 100644
--- a/licensecheck/cli.py
+++ b/licensecheck/cli.py
@@ -12,7 +12,8 @@ from sys import stdin, stdout
 
 from fhconfparser import FHConfParser, SimpleConf
 
-from licensecheck import formatter, get_deps, license_matrix, packageinfo, types
+from licensecheck import fmt, get_deps, packageinfo, types
+from licensecheck import license_matrix
 
 stdout.reconfigure(encoding="utf-8")  # type: ignore[general-type-issues]
 
@@ -23,12 +24,13 @@ def cli() -> None:  # pragma: no cover
 	parser.add_argument(
 		"--license",
 		"-l",
-		help="",
+		help="Specify the project license explicitly, rather than rely on "
+		"licensecheck interpreting this from pyproject.toml",
 	)
 	parser.add_argument(
 		"--format",
 		"-f",
-		help=f"Output format. one of: {', '.join(list(formatter.formatMap))}. default=simple",
+		help=f"Output format. one of: {', '.join(list(fmt.formatMap))}. default=simple",
 	)
 	parser.add_argument(
 		"--requirements-paths",
@@ -45,47 +47,57 @@ def cli() -> None:  # pragma: no cover
 	parser.add_argument(
 		"--file",
 		"-o",
-		help="Filename to write to (omit for stdout)",
+		help="Filename to write output to (omit this for stdout)",
 	)
 	parser.add_argument(
 		"--ignore-packages",
-		help="a list of packages to ignore (compat=True)",
+		help="List of packages/dependencies to ignore (compat=True)",
 		nargs="+",
 	)
 	parser.add_argument(
 		"--fail-packages",
-		help="a list of packages to fail (compat=False)",
+		help="List of packages/dependencies to fail (compat=False)",
 		nargs="+",
 	)
 	parser.add_argument(
 		"--ignore-licenses",
-		help="a list of licenses to ignore (skipped, compat may still be False)",
+		help="List of licenses to ignore (skipped, compat may still be False)",
 		nargs="+",
 	)
 	parser.add_argument(
 		"--fail-licenses",
-		help="a list of licenses to fail (compat=False)",
+		help="List of licenses to fail (compat=False)",
 		nargs="+",
 	)
 	parser.add_argument(
 		"--only-licenses",
-		help="a list of allowed licenses (any other license will fail)",
+		help="List of allowed licenses (packages/dependencies with any other license will fail)",
 		nargs="+",
 	)
 	parser.add_argument(
 		"--skip-dependencies",
-		help="a list of packages to skip (compat=True)",
+		help="List of packages/dependencies to skip (this sets the 'compatability' to True)",
 		nargs="+",
 	)
 	parser.add_argument(
 		"--hide-output-parameters",
-		help="a list of parameters to hide from the produced output",
+		help="List of parameters to hide from the produced output",
 		nargs="+",
 	)
+	parser.add_argument(
+		"--show-only-failing",
+		help="Only output a list of incompatible/ failing packages from this lib",
+		action="store_true",
+	)
+	parser.add_argument(
+		"--pypi-api",
+		help="Specify a custom pypi api endpoint, for example if using a custom pypi server",
+		default="https://pypi.org/pypi/",
+	)
 	parser.add_argument(
 		"--zero",
 		"-0",
-		help="Return non zero exit code if an incompatible license is found",
+		help="Return non zero exit code if an incompatible license is found, ideal for CI/CD",
 		action="store_true",
 	)
 	args = vars(parser.parse_args())
@@ -98,7 +110,7 @@ def cli() -> None:  # pragma: no cover
 	sysexit(ec)
 
 
-def main(args: dict) -> int:
+def main(args: dict| argparse.Namespace) -> int:
 	"""Test entry point.
 
 	Note: FHConfParser (Parses in the following order: `pyproject.toml`,
@@ -138,10 +150,13 @@ def main(args: dict) -> int:
 	def getFromConfig(key: str) -> list[types.ucstr]:
 		return list(map(types.ucstr, simpleConf.get(key, [])))
 
+	package_info_manager = packageinfo.PackageInfoManager(simpleConf.get("pypi_api"))
+
 	incompatible, depsWithLicenses = get_deps.check(
 		requirements_paths=requirements_paths,
 		groups=simpleConf.get("groups", []),
 		this_license=this_license,
+		package_info_manager=package_info_manager,
 		ignore_packages=getFromConfig("ignore_packages"),
 		fail_packages=getFromConfig("fail_packages"),
 		ignore_licenses=getFromConfig("ignore_licenses"),
@@ -159,12 +174,14 @@ def main(args: dict) -> int:
 			f"Valid parameters are: {', '.join(available_params)}"
 		)
 		raise ValueError(msg)
-	if simpleConf.get("format", "simple") in formatter.formatMap:
+	if simpleConf.get("format", "simple") in fmt.formatMap:
 		print(
-			formatter.formatMap[simpleConf.get("format", "simple")](
+			fmt.fmt(
+				simpleConf.get("format", "simple"),
 				this_license,
 				sorted(depsWithLicenses),
 				hide_output_parameters,
+				show_only_failing=args.get("show_only_failing", False),
 			),
 			file=output_file,
 		)
diff --git a/licensecheck/fmt.py b/licensecheck/fmt.py
new file mode 100644
index 0000000..126207c
--- /dev/null
+++ b/licensecheck/fmt.py
@@ -0,0 +1,327 @@
+"""The formatter is reponsible for outputting the list of PackageInfo[s].
+
+Note the PackageInfo has the following attributes, that we can use to build each output format:
+- name: str
+- version: str
+- namever: str
+- size: int = -1
+- homePage: str
+- author: str
+- license: ucstr
+- licenseCompat: bool
+- errorCode: int = 0
+
+
+The available output formats are defined as follows
+
+
+- ansi: Plain text output with ANSI color codes for terminal display.
+	used for simple, color-coded output on the command line.
+- plain: A basic, no-frills plain text format, used when a clean and simple
+	textual representation is needed without any additional markup or styling.
+- markdown: A lightweight markup language with plain-text formatting syntax. Ideal
+	for creating formatted documents that can be easily converted into HTML for web display.
+- html: A format suitable for rendering in web browsers. (can be styled with CSS
+	for more complex presentation.)
+- json: A structured data format. This format representing the list of PackageInfo[s]
+	as a JSON object
+- csv: A simple, comma-separated values format. widely used in spreadsheets and
+	databases for easy import/export of data.
+
+Note that these support the get_filtered_dict method, which allows users
+to hide some of the output via the `--hide-output-parameters` cli flag. In addition
+these support the show_only_failing method, which allows users
+to show only packages that are not compatible with out license via the
+`--show-only-failing` cli flag
+
+"""
+
+from __future__ import annotations
+
+import csv
+import json
+import re
+from collections import OrderedDict
+from importlib.metadata import PackageNotFoundError, version
+from io import StringIO
+from pathlib import Path
+from typing import Any
+
+import markdown as markdownlib
+from rich.console import Console
+from rich.table import Table
+
+from licensecheck.types import License, PackageInfo, ucstr
+
+THISDIR = Path(__file__).resolve().parent
+
+try:
+	VERSION = version("licensecheck")
+except PackageNotFoundError:
+	VERSION = "dev"
+INFO = {"program": "licensecheck", "version": VERSION, "license": "MIT LICENSE"}
+
+
+def _printLicense(licenseEnum: License) -> str:
+	"""Output a license as plain text.
+
+	:param License licenseEnum: License
+	:return str: license of plain text
+	"""
+
+	licenseMap = {
+		License.PUBLIC: "PUBLIC DOMAIN/ CC-PDDC/ CC0-1.0",
+		License.UNLICENSE: "UNLICENSE/ WTFPL",
+		License.BOOST: "BOOST/ BSL-1.0",
+		License.MIT: "MIT",
+		License.BSD: "BSD",
+		License.ISC: "ISC",
+		License.NCSA: "NCSA",
+		License.PSFL: "PYTHON/ PSF-2.0",
+		License.APACHE: "APACHE",
+		License.ECLIPSE: "ECLIPSE",
+		License.ACADEMIC_FREE: "AFL",
+		License.LGPL_2_PLUS: "LGPLV2+/ LGPL-2.0-OR-LATER",
+		License.LGPL_3_PLUS: "LGPLV3+/ LGPL-3.0-OR-LATER",
+		License.LGPL_2: "LGPL-2.0-ONLY/ LGPLV2",
+		License.LGPL_3: "LGPL-3.0-ONLY/ LGPLV3",
+		License.LGPL_X: "LGPL",
+		License.AGPL_3_PLUS: "AGPL",
+		License.GPL_2_PLUS: "GPL-2.0-OR-LATER/ GPLV2+",
+		License.GPL_3_PLUS: "GPL-3.0-OR-LATER/ GPLV3+",
+		License.GPL_2: "GPLV2/ GPL-2.0",
+		License.GPL_3: "GPLV3/ GPL-3.0",
+		License.GPL_X: "GPL",
+		License.MPL: "MPL",
+		License.EU: "EUPL",
+		License.PROPRIETARY: "PROPRIETARY",
+		License.NO_LICENSE: "NO LICENSE/ UNKNOWN",
+	}
+
+	if licenseEnum not in licenseMap:
+		return "NO LICENSE/ UNKNOWN LICENSE"
+
+	return f"{licenseMap[licenseEnum]} LICENSE"
+
+
+def stripAnsi(string: str) -> str:
+	"""Strip ansi codes from a given string.
+
+	Args:
+	----
+		string (str): string to strip codes from
+
+	Returns:
+	-------
+		str: plaintext, utf-8 string (safe for writing to files)
+
+	"""
+	return re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").sub("", string)
+
+
+def ansi(
+	myLice: License,
+	packages: list[dict[str, Any]],
+) -> str:
+	"""Format to ansi.
+
+	:param License myLice: project license
+	:param list[dict[str, Any]] packages: list of PackageCompats to format.
+	:return str: string to send to specified output in ansi format
+	"""
+
+	string = StringIO()
+
+	console = Console(file=string, color_system="truecolor", safe_box=False)
+
+	table = Table(title="\nInfo")
+	table.add_column("Item", style="cyan")
+	table.add_column("Value", style="magenta")
+	_ = [table.add_row(k, v) for k, v in INFO.items()]
+	table.add_row("project_license", _printLicense(myLice))
+
+	console.print(table)
+
+	if len(packages) == 0:
+		return f"{string.getvalue()}\nNo packages"
+
+	errors = [x for x in packages if x.get("errorCode", 0) > 0]
+	if len(errors) > 0:
+		table = Table(title="\nList Of Errors")
+		table.add_column("Package", style="magenta")
+		_ = [table.add_row(x.get("name", "?")) for x in errors]
+		console.print(table)
+
+	table = Table(title="\nList Of Packages")
+	if licensecompat_bool := "licenseCompat" in packages[0]:
+		table.add_column("Compatible", style="cyan")
+	if name_bool := "name" in packages[0]:
+		table.add_column("Package", style="magenta")
+	if license_bool := "license" in packages[0]:
+		table.add_column("License(s)", style="magenta")
+	licenseCompat = (
+		"[red]✖[/]",
+		"[green]✔[/]",
+	)
+	_ = [
+		table.add_row(
+			*(
+				([licenseCompat[x.get("licenseCompat", 0)]] if licensecompat_bool else [])
+				+ ([x.get("name")] if name_bool else [])
+				+ ([x.get("license")] if license_bool else [])
+			)
+		)
+		for x in packages
+	]
+	console.print(table)
+	return string.getvalue()
+
+
+def plainText(
+	myLice: License,
+	packages: list[dict[str, Any]],
+) -> str:
+	"""Format to plain text.
+
+	:param License myLice: project license
+	:param list[dict[str, Any]] packages: list of PackageCompats to format.
+	:return str: string to send to specified output in plain text format
+
+	"""
+	return stripAnsi(ansi(myLice, packages))
+
+
+def markdown(
+	myLice: License,
+	packages: list[dict[str, Any]],
+) -> str:
+	"""Format to markdown.
+
+	:param License myLice: project license
+	:param list[dict[str, Any]] packages: list of PackageCompats to format.
+	:return str: string to send to specified output in markdown format
+	"""
+
+	info = "\n".join(f"- {k}: {v}" for k, v in INFO.items())
+	strBuf = [f"## Info\n\n{info}\n\n## Project License\n\n{_printLicense(myLice)}\n"]
+
+	if len(packages) == 0:
+		return f"{strBuf[0]}\nNo packages"
+
+	strBuf.append("## Packages\n\nFind a list of packages below\n")
+	packages = sorted(packages, key=lambda i: i.get("name", "?"))
+
+	# Summary Table
+	strBuf.append("|Compatible|Package|\n|:--|:--|")
+	strBuf.extend(
+		[f"|{'✔' if pkg.get('licenseCompat') else '✖'}|{pkg.get('name')}|" for pkg in packages]
+	)
+
+	# Details
+	params_use_in_markdown = {
+		"homePage": "HomePage",
+		"author": "Author",
+		"license": "License",
+		"licenseCompat": "Compatible",
+		"size": "Size",
+	}
+	for pkg in packages:
+		pkg_ordered_dict = OrderedDict(
+			(param, pkg[param]) for param in params_use_in_markdown if param in pkg
+		)
+		strBuf.extend(
+			[
+				f"\n### {pkg.get('namever')}\n",
+				*(f"- {params_use_in_markdown[k]}: {v}" for k, v in pkg_ordered_dict.items()),
+			]
+		)
+	return "\n".join(strBuf) + "\n"
+
+
+def html(
+	myLice: License,
+	packages: list[dict[str, Any]],
+) -> str:
+	"""Format to html.
+
+	:param License myLice: project license
+	:param list[dict[str, Any]] packages: list of PackageCompats to format.
+	:return str: string to send to specified output in html format
+	"""
+	html = markdownlib.markdown(
+		markdown(myLice=myLice, packages=packages),
+		extensions=["tables"],
+	)
+	return (THISDIR / "html.template").read_text("utf-8").replace("{html}", html)
+
+
+def raw(myLice: License, packages: list[dict[str, Any]]) -> str:
+	"""Format to json.
+
+	:param License myLice: project license
+	:param list[dict[str, Any]] packages: list of PackageCompats to format.
+	:return str: string to send to specified output in json format
+	"""
+
+	return json.dumps(
+		{
+			"info": INFO,
+			"project_license": _printLicense(myLice),
+			"packages": packages,
+		},
+		indent="\t",
+	)
+
+
+def rawCsv(
+	myLice: License,
+	packages: list[dict[str, Any]],
+) -> str:
+	"""Format to csv.
+
+	:param License myLice: project license
+	:param list[dict[str, Any]] packages: list of PackageCompats to format.
+	:return str: string to send to specified output in csv format
+	"""
+
+	_ = myLice
+	string = StringIO()
+	writer = csv.DictWriter(string, fieldnames=list(packages[0]), lineterminator="\n")
+	writer.writeheader()
+	writer.writerows(packages)
+	return string.getvalue()
+
+
+def fmt(
+	format_: str,
+	myLice: License,
+	packages: list[PackageInfo],
+	hide_parameters: list[ucstr] | None = None,
+	*,
+	show_only_failing: bool = False,
+) -> str:
+	"""Format to a given format by `format_`.
+
+	:param License myLice: project license
+	:param list[PackageInfo] packages: list of PackageCompats to format.
+	:param list[ucstr] hide_parameters: list of parameters to ignore in the output.
+	:param bool show_only_failing: output only failing packages, defaults to False.
+	:return str: string to send to specified output in ansi format
+	"""
+	hide_parameters = hide_parameters or []
+	if show_only_failing:
+		packages = [x for x in packages if not x.licenseCompat]
+
+	pkgs: list[dict[str, Any]] = [x.get_filtered_dict(hide_parameters) for x in packages]
+
+	return formatMap.get(format_, plainText)(myLice, pkgs)
+
+
+formatMap = {
+	"json": raw,
+	"markdown": markdown,
+	"html": html,
+	"csv": rawCsv,
+	"ansi": ansi,
+	"simple": plainText,
+}
diff --git a/licensecheck/formatter.py b/licensecheck/formatter.py
deleted file mode 100644
index 689d2aa..0000000
--- a/licensecheck/formatter.py
+++ /dev/null
@@ -1,343 +0,0 @@
-"""Output.
-
-```json
-{
-	name: str
-	version: str
-	namever: str
-	size: int
-	homePage: str
-	author: str
-	license: ucstr
-	licenseCompat: bool
-}
-```
-
-To one of the following formats:
-
-- ansi
-- plain
-- markdown
-- html
-- json
-- csv
-"""
-
-from __future__ import annotations
-
-import csv
-import json
-import re
-from collections import OrderedDict
-from importlib.metadata import PackageNotFoundError, version
-from io import StringIO
-from pathlib import Path
-
-import markdown as markdownlib
-from rich.console import Console
-from rich.table import Table
-
-from licensecheck.types import License, PackageInfo, ucstr
-
-THISDIR = Path(__file__).resolve().parent
-
-try:
-	VERSION = version("licensecheck")
-except PackageNotFoundError:
-	VERSION = "dev"
-INFO = {"program": "licensecheck", "version": VERSION, "license": "MIT LICENSE"}
-
-
-def _printLicense(licenseEnum: License) -> str:
-	"""Output a license as plain text.
-
-	:param License licenseEnum: License
-	:return str: license of plain text
-	"""
-
-	licenseMap = {
-		License.PUBLIC: "PUBLIC DOMAIN/ CC-PDDC/ CC0-1.0",
-		License.UNLICENSE: "UNLICENSE/ WTFPL",
-		License.BOOST: "BOOST/ BSL-1.0",
-		License.MIT: "MIT",
-		License.BSD: "BSD",
-		License.ISC: "ISC",
-		License.NCSA: "NCSA",
-		License.PSFL: "PYTHON/ PSF-2.0",
-		License.APACHE: "APACHE",
-		License.ECLIPSE: "ECLIPSE",
-		License.ACADEMIC_FREE: "AFL",
-		License.LGPL_2_PLUS: "LGPLV2+/ LGPL-2.0-OR-LATER",
-		License.LGPL_3_PLUS: "LGPLV3+/ LGPL-3.0-OR-LATER",
-		License.LGPL_2: "LGPL-2.0-ONLY/ LGPLV2",
-		License.LGPL_3: "LGPL-3.0-ONLY/ LGPLV3",
-		License.LGPL_X: "LGPL",
-		License.AGPL_3_PLUS: "AGPL",
-		License.GPL_2_PLUS: "GPL-2.0-OR-LATER/ GPLV2+",
-		License.GPL_3_PLUS: "GPL-3.0-OR-LATER/ GPLV3+",
-		License.GPL_2: "GPLV2/ GPL-2.0",
-		License.GPL_3: "GPLV3/ GPL-3.0",
-		License.GPL_X: "GPL",
-		License.MPL: "MPL",
-		License.EU: "EUPL",
-		License.PROPRIETARY: "PROPRIETARY",
-		License.NO_LICENSE: "NO LICENSE/ UNKNOWN",
-	}
-
-	if licenseEnum not in licenseMap:
-		return "NO LICENSE/ UNKNOWN LICENSE"
-
-	return f"{licenseMap[licenseEnum]} LICENSE"
-
-
-def stripAnsi(string: str) -> str:
-	"""Strip ansi codes from a given string.
-
-	Args:
-	----
-		string (str): string to strip codes from
-
-	Returns:
-	-------
-		str: plaintext, utf-8 string (safe for writing to files)
-
-	"""
-	return re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").sub("", string)
-
-
-def ansi(
-	myLice: License,
-	packages: list[PackageInfo],
-	hide_parameters: list[ucstr] | None = None,
-) -> str:
-	"""Format to ansi.
-
-	Args:
-	----
-		myLice (License): project license
-		packages (list[PackageInfo]): list of PackageCompats to format.
-		hide_parameters (list[str]): list of parameters to ignore in the output.
-
-	Returns:
-	-------
-		str: string to send to specified output in ansi format
-
-	"""
-	if hide_parameters is None:
-		hide_parameters = []
-	string = StringIO()
-
-	console = Console(file=string, color_system="truecolor", safe_box=False)
-
-	table = Table(title="\nInfo")
-	table.add_column("Item", style="cyan")
-	table.add_column("Value", style="magenta")
-	_ = [table.add_row(k, v) for k, v in INFO.items()]
-	table.add_row("project_license", _printLicense(myLice))
-
-	console.print(table)
-
-	if len(packages) == 0:
-		return f"{string.getvalue()}\nNo packages"
-
-	errors = [x for x in packages if x.errorCode > 0]
-	if len(errors) > 0:
-		table = Table(title="\nList Of Errors")
-		table.add_column("Package", style="magenta")
-		_ = [table.add_row(x.name) for x in errors]
-		console.print(table)
-
-	table = Table(title="\nList Of Packages")
-	if licensecompat_bool := "LICENSECOMPAT" not in hide_parameters:
-		table.add_column("Compatible", style="cyan")
-	if name_bool := "NAME" not in hide_parameters:
-		table.add_column("Package", style="magenta")
-	if license_bool := "LICENSE" not in hide_parameters:
-		table.add_column("License(s)", style="magenta")
-	licenseCompat = (
-		"[red]✖[/]",
-		"[green]✔[/]",
-	)
-	_ = [
-		table.add_row(
-			*(
-				([licenseCompat[x.licenseCompat]] if licensecompat_bool else [])
-				+ ([x.name] if name_bool else [])
-				+ ([x.license] if license_bool else [])
-			)
-		)
-		for x in packages
-	]
-	console.print(table)
-	return string.getvalue()
-
-
-def plainText(
-	myLice: License,
-	packages: list[PackageInfo],
-	hide_parameters: list[ucstr] | None = None,
-) -> str:
-	"""Format to ansi.
-
-	Args:
-	----
-		myLice (License): project license
-		packages (list[PackageInfo]): list of PackageCompats to format.
-		hide_parameters (list[str]): list of parameters to ignore in the output.
-
-	Returns:
-	-------
-		str: string to send to specified output in plain text format
-
-	"""
-	if hide_parameters is None:
-		hide_parameters = []
-	return stripAnsi(ansi(myLice, packages, hide_parameters))
-
-
-def markdown(
-	myLice: License,
-	packages: list[PackageInfo],
-	hide_parameters: list[ucstr] | None = None,
-) -> str:
-	"""Format to markdown.
-
-	Args:
-	----
-		myLice (License): project license
-		packages (list[PackageInfo]): list of PackageCompats to format.
-		hide_parameters (list[str]): list of parameters to ignore in the output.
-
-	Returns:
-	-------
-		str: string to send to specified output in markdown format
-
-	"""
-	if hide_parameters is None:
-		hide_parameters = []
-	info = "\n".join(f"- **{k}**: {v}" for k, v in INFO.items())
-	strBuf = [f"## Info\n\n{info}\n\n## Project License\n\n{_printLicense(myLice)}\n"]
-
-	if len(packages) == 0:
-		return f"{strBuf[0]}\nNo packages"
-
-	strBuf.append("## Packages\n\nFind a list of packages below\n")
-	packages = sorted(packages, key=lambda i: i.name)
-
-	# Summary Table
-	strBuf.append("|Compatible|Package|\n|:--|:--|")
-	strBuf.extend([f"|{'✔' if pkg.licenseCompat else '✖'}|{pkg.name}|" for pkg in packages])
-
-	# Details
-	params_use_in_markdown = {
-		"homePage": "HomePage",
-		"author": "Author",
-		"license": "License",
-		"licenseCompat": "Compatible",
-		"size": "Size",
-	}
-	for pkg in packages:
-		pkg_dict = pkg.get_filtered_dict(hide_parameters)
-		pkg_dict_ordered_dict = OrderedDict(
-			(param, pkg_dict[param]) for param in params_use_in_markdown if param in pkg_dict
-		)
-		strBuf.extend(
-			[
-				f"\n### {pkg.namever}\n",
-				*(f"- {params_use_in_markdown[k]}: {v}" for k, v in pkg_dict_ordered_dict.items()),
-			]
-		)
-	return "\n".join(strBuf) + "\n"
-
-
-def html(
-	myLice: License,
-	packages: list[PackageInfo],
-	hide_parameters: list[ucstr] | None = None,
-) -> str:
-	"""Format to html.
-
-	Args:
-	----
-		myLice (License): project license
-		packages (list[PackageInfo]): list of PackageCompats to format.
-		hide_parameters (list[str]): list of parameters to ignore in the output.
-
-	Returns:
-	-------
-		str: string to send to specified output in html format
-
-	"""
-	html = markdownlib.markdown(
-		markdown(myLice=myLice, packages=packages, hide_parameters=hide_parameters),
-		extensions=["tables"],
-	)
-	return (THISDIR / "html.template").read_text("utf-8").replace("{html}", html)
-
-
-def raw(
-	myLice: License,
-	packages: list[PackageInfo],
-	hide_parameters: list[ucstr] | None = None,
-) -> str:
-	"""Format to json.
-
-	Args:
-	----
-		myLice (License): project license
-		packages (list[PackageInfo]): list of PackageCompats to format.
-		hide_parameters (list[str]): list of parameters to ignore in the output.
-
-	Returns:
-	-------
-		str: string to send to specified output in raw json format
-
-	"""
-	if hide_parameters is None:
-		hide_parameters = []
-	return json.dumps(
-		{
-			"info": INFO,
-			"project_license": _printLicense(myLice),
-			"packages": [x.get_filtered_dict(hide_parameters) for x in packages],
-		},
-		indent="\t",
-	)
-
-
-def rawCsv(
-	myLice: License,
-	packages: list[PackageInfo],
-	hide_parameters: list[ucstr] | None = None,
-) -> str:
-	"""Format to csv.
-
-	Args:
-	----
-		myLice (License): project license
-		packages (list[PackageInfo]): list of PackageCompats to format.
-		hide_parameters (list[str]): list of parameters to ignore in the output.
-
-	Returns:
-	-------
-		str: string to send to specified output in raw csv format
-
-	"""
-	if hide_parameters is None:
-		hide_parameters = []
-	_ = myLice
-	string = StringIO()
-	writer = csv.DictWriter(string, fieldnames=list(packages[0].__dict__), lineterminator="\n")
-	writer.writeheader()
-	writer.writerows([x.get_filtered_dict(hide_parameters) for x in packages])
-	return string.getvalue()
-
-
-formatMap = {
-	"json": raw,
-	"markdown": markdown,
-	"html": html,
-	"csv": rawCsv,
-	"ansi": ansi,
-	"simple": plainText,
-}
diff --git a/licensecheck/get_deps.py b/licensecheck/get_deps.py
index ed185b7..5b70ece 100644
--- a/licensecheck/get_deps.py
+++ b/licensecheck/get_deps.py
@@ -6,7 +6,8 @@ from pathlib import Path
 
 import tomli
 
-from licensecheck import license_matrix, packageinfo
+from licensecheck import license_matrix
+from licensecheck.packageinfo import PackageInfoManager
 from licensecheck.resolvers import native as res_native
 from licensecheck.resolvers import uv as res_uv
 from licensecheck.types import JOINS, License, PackageInfo, ucstr
@@ -42,6 +43,7 @@ def check(
 	requirements_paths: list[str],
 	groups: list[str],
 	this_license: License,
+	package_info_manager: PackageInfoManager,
 	ignore_packages: list[ucstr] | None = None,
 	fail_packages: list[ucstr] | None = None,
 	ignore_licenses: list[ucstr] | None = None,
@@ -69,7 +71,7 @@ def check(
 		onlyLicensesType.remove(License.NO_LICENSE)
 
 	# Check it is compatible with packages and add a note
-	packages = packageinfo.getPackages(requirements)
+	packages = package_info_manager.getPackages(requirements)
 	for package in packages:
 		# Deal with --ignore-packages and --fail-packages
 		package.licenseCompat = False
diff --git a/licensecheck/packageinfo.py b/licensecheck/packageinfo.py
index f144e32..98f3790 100644
--- a/licensecheck/packageinfo.py
+++ b/licensecheck/packageinfo.py
@@ -15,84 +15,111 @@ from licensecheck.session import session
 from licensecheck.types import JOINS, UNKNOWN, PackageInfo, ucstr
 
 
-def _pkgMetadataGet(pkgMetadata: metadata.PackageMetadata | dict[str, Any], key: str) -> str:
-	"""Get a string from a key from pkgMetadata."""
-	value = pkgMetadata.get(key, UNKNOWN)
-	if not isinstance(value, str) and isinstance(value, Iterable):
-		value = JOINS.join(str(x) for x in value)
-	return str(value) or UNKNOWN
+class PackageInfoManager:
+	def __init__(self, pypi_api: str) -> None:
+		self.pypi_api = pypi_api
 
+	def getPackages(self, reqs: set[ucstr]) -> set[PackageInfo]:
+		"""Get dependency info.
 
-def getPackageInfoLocal(requirement: ucstr) -> PackageInfo:
-	"""Get package info from local files including version, author
-	and	the license.
+		Args:
+		----
+			reqs (set[ucstr]): set of dependency names to gather info on
 
-	:param str requirement: name of the package
-	:raises ModuleNotFoundError: if the package does not exist
-	:return PackageInfo: package information
-	"""
-	try:
-		# Get pkg metadata,, license, homepage, and author
-		pkgMetadata = metadata.metadata(requirement)
-		lice = licenseFromClassifierlist(pkgMetadata.get_all("Classifier"))
-		if lice == UNKNOWN:
-			lice = _pkgMetadataGet(pkgMetadata, "License")
-		homePage = _pkgMetadataGet(pkgMetadata, "Home-page")
-		author = _pkgMetadataGet(pkgMetadata, "Author")
-		name = _pkgMetadataGet(pkgMetadata, "Name")
-		version = _pkgMetadataGet(pkgMetadata, "Version")
-		size = 0
-		packagePaths = metadata.Distribution.from_name(requirement).files
-		if packagePaths is not None:
-			size = sum(pp.size for pp in packagePaths if pp.size is not None)
-
-		# append to pkgInfo
-		return PackageInfo(
-			name=name,
-			version=version,
-			homePage=homePage,
-			author=author,
-			size=size,
-			license=ucstr(lice),
-		)
+		Returns:
+		-------
+			set[PackageInfo]: set of dependencies
 
-	except metadata.PackageNotFoundError as error:
-		raise ModuleNotFoundError from error
+		"""
+		packageinfo = set()
+		for requirement in reqs:
+			try:
+				packageinfo.add(self.getPackageInfoLocal(requirement))
+			except ModuleNotFoundError:
+				try:
+					packageinfo.add(self.getPackageInfoPypi(requirement))
+				except ModuleNotFoundError:
+					packageinfo.add(PackageInfo(name=requirement, errorCode=1))
 
+		return packageinfo
 
-def getPackageInfoPypi(requirement: ucstr) -> PackageInfo:
-	"""Get package info from local files including version, author
-	and	the license.
+	def getPackageInfoLocal(self, requirement: ucstr) -> PackageInfo:
+		"""Get package info from local files including version, author
+		and	the license.
 
-	:param str requirement: name of the package
-	:raises ModuleNotFoundError: if the package does not exist
-	:return PackageInfo: package information
-	"""
-	request = session.get(f"https://pypi.org/pypi/{requirement}/json", timeout=60)
-	response = request.json()
-	try:
-		info = response.get("info", {})
-		licenseClassifier = licenseFromClassifierlist(info["classifiers"])
-
-		size = -1
-		urls = response.get("urls", [])
-		if urls:
-			size = int(urls[-1]["size"])
-
-		return PackageInfo(
-			name=_pkgMetadataGet(info, "name"),
-			version=_pkgMetadataGet(info, "version"),
-			homePage=_pkgMetadataGet(info, "home_page"),
-			author=_pkgMetadataGet(info, "author"),
-			size=size,
-			license=ucstr(
-				licenseClassifier
-				if licenseClassifier != UNKNOWN
-				else info.get("license", UNKNOWN) or UNKNOWN
-			),
-		)
-	except KeyError as error:
-		raise ModuleNotFoundError from error
+		:param str requirement: name of the package
+		:raises ModuleNotFoundError: if the package does not exist
+		:return PackageInfo: package information
+		"""
+		try:
+			# Get pkg metadata,, license, homepage, and author
+			pkgMetadata = metadata.metadata(requirement)
+			lice = licenseFromClassifierlist(pkgMetadata.get_all("Classifier"))
+			if lice == UNKNOWN:
+				lice = _pkgMetadataGet(pkgMetadata, "License")
+			homePage = _pkgMetadataGet(pkgMetadata, "Home-page")
+			author = _pkgMetadataGet(pkgMetadata, "Author")
+			name = _pkgMetadataGet(pkgMetadata, "Name")
+			version = _pkgMetadataGet(pkgMetadata, "Version")
+			size = 0
+			packagePaths = metadata.Distribution.from_name(requirement).files
+			if packagePaths is not None:
+				size = sum(pp.size for pp in packagePaths if pp.size is not None)
+
+			# append to pkgInfo
+			return PackageInfo(
+				name=name,
+				version=version,
+				homePage=homePage,
+				author=author,
+				size=size,
+				license=ucstr(lice),
+			)
+
+		except metadata.PackageNotFoundError as error:
+			raise ModuleNotFoundError from error
+
+	def getPackageInfoPypi(self, requirement: ucstr) -> PackageInfo:
+		"""Get package info from local files including version, author
+		and	the license.
+
+		:param str requirement: name of the package
+		:raises ModuleNotFoundError: if the package does not exist
+		:return PackageInfo: package information
+		"""
+		request = session.get(f"{self.pypi_api}{requirement}/json", timeout=60)
+		response = request.json()
+		try:
+			info = response.get("info", {})
+			licenseClassifier = licenseFromClassifierlist(info["classifiers"])
+
+			size = -1
+			urls = response.get("urls", [])
+			if urls:
+				size = int(urls[-1]["size"])
+
+			return PackageInfo(
+				name=_pkgMetadataGet(info, "name"),
+				version=_pkgMetadataGet(info, "version"),
+				homePage=_pkgMetadataGet(info, "home_page"),
+				author=_pkgMetadataGet(info, "author"),
+				size=size,
+				license=ucstr(
+					licenseClassifier
+					if licenseClassifier != UNKNOWN
+					else info.get("license", UNKNOWN) or UNKNOWN
+				),
+			)
+		except KeyError as error:
+			raise ModuleNotFoundError from error
+
+
+def _pkgMetadataGet(pkgMetadata: metadata.PackageMetadata | dict[str, Any], key: str) -> str:
+	"""Get a string from a key from pkgMetadata."""
+	value = pkgMetadata.get(key, UNKNOWN)
+	if not isinstance(value, str) and isinstance(value, Iterable):
+		value = JOINS.join(str(x) for x in value)
+	return str(value) or UNKNOWN
 
 
 def licenseFromClassifierlist(classifiers: list[str] | None | list[Any]) -> ucstr:
@@ -119,31 +146,6 @@ def licenseFromClassifierlist(classifiers: list[str] | None | list[Any]) -> ucst
 	return ucstr(JOINS.join(licenses) if len(licenses) > 0 else UNKNOWN)
 
 
-def getPackages(reqs: set[ucstr]) -> set[PackageInfo]:
-	"""Get dependency info.
-
-	Args:
-	----
-		reqs (set[ucstr]): set of dependency names to gather info on
-
-	Returns:
-	-------
-		set[PackageInfo]: set of dependencies
-
-	"""
-	packageinfo = set()
-	for requirement in reqs:
-		try:
-			packageinfo.add(getPackageInfoLocal(requirement))
-		except ModuleNotFoundError:
-			try:
-				packageinfo.add(getPackageInfoPypi(requirement))
-			except ModuleNotFoundError:
-				packageinfo.add(PackageInfo(name=requirement, errorCode=1))
-
-	return packageinfo
-
-
 def getMyPackageMetadata() -> dict[str, Any]:
 	"""Get the package classifiers and license from "setup.cfg", "pyproject.toml".
 
@@ -156,7 +158,10 @@ def getMyPackageMetadata() -> dict[str, Any]:
 		config = configparser.ConfigParser()
 		config.read("setup.cfg")
 		if "metadata" in config.sections() and "license" in config["metadata"]:
-			classifiers = config.get("metadata", "classifiers").strip().splitlines()
+			if "classifier" in config["metadata"]:
+				classifiers = config.get("metadata", "classifier").strip().splitlines()
+			else:
+				classifiers = config.get("metadata", "classifiers").strip().splitlines()
 			licenseStr = ucstr(config.get("metadata", "license"))
 			return {"classifiers": classifiers, "license": licenseStr}
 
diff --git a/licensecheck/types.py b/licensecheck/types.py
index 65ac037..a3c9cc2 100644
--- a/licensecheck/types.py
+++ b/licensecheck/types.py
@@ -89,6 +89,7 @@ class License(Enum):
 	EU = 61
 	# PROPRIETARY
 	PROPRIETARY = 190
+	UNKNOWN = 191
 	# No License
 	NO_LICENSE = 200
 


Test patch:
diff --git a/tests/platform_independent/data/advanced.json b/tests/platform_independent/data/advanced.json
index 64a0369..23c96a5 100644
--- a/tests/platform_independent/data/advanced.json
+++ b/tests/platform_independent/data/advanced.json
@@ -29,4 +29,4 @@
 			"namever": "example1-UNKNOWN"
 		}
 	]
-}
+}
\ No newline at end of file
diff --git a/tests/platform_independent/data/advanced.md b/tests/platform_independent/data/advanced.md
index 01f6dc8..9dffb45 100644
--- a/tests/platform_independent/data/advanced.md
+++ b/tests/platform_independent/data/advanced.md
@@ -1,8 +1,8 @@
 ## Info
 
-- **program**: licensecheck
-- **version**: dev
-- **license**: MIT LICENSE
+- program: licensecheck
+- version: dev
+- license: MIT LICENSE
 
 ## Project License
 
diff --git a/tests/platform_independent/data/advanced.txt b/tests/platform_independent/data/advanced.txt
index de36265..3d6ce5e 100644
--- a/tests/platform_independent/data/advanced.txt
+++ b/tests/platform_independent/data/advanced.txt
@@ -1,5 +1,5 @@
-
-               Info
+                                  
+               Info               
 ┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
 ┃ Item            ┃ Value        ┃
 ┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
@@ -8,16 +8,16 @@
 │ license         │ MIT LICENSE  │
 │ project_license │ MIT LICENSE  │
 └─────────────────┴──────────────┘
-
-  List Of
-   Errors
+            
+  List Of   
+   Errors   
 ┏━━━━━━━━━━┓
 ┃ Package  ┃
 ┡━━━━━━━━━━┩
 │ example1 │
 └──────────┘
-
-           List Of Packages
+                                      
+           List Of Packages           
 ┏━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━┓
 ┃ Compatible ┃ Package  ┃ License(s) ┃
 ┡━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━┩
diff --git a/tests/platform_independent/data/advanced_ignore_params.json b/tests/platform_independent/data/advanced_ignore_params.json
index 1790d8b..e182cca 100644
--- a/tests/platform_independent/data/advanced_ignore_params.json
+++ b/tests/platform_independent/data/advanced_ignore_params.json
@@ -25,4 +25,4 @@
 			"namever": "example1-UNKNOWN"
 		}
 	]
-}
+}
\ No newline at end of file
diff --git a/tests/platform_independent/data/advanced_ignore_params.md b/tests/platform_independent/data/advanced_ignore_params.md
index 01f6dc8..9dffb45 100644
--- a/tests/platform_independent/data/advanced_ignore_params.md
+++ b/tests/platform_independent/data/advanced_ignore_params.md
@@ -1,8 +1,8 @@
 ## Info
 
-- **program**: licensecheck
-- **version**: dev
-- **license**: MIT LICENSE
+- program: licensecheck
+- version: dev
+- license: MIT LICENSE
 
 ## Project License
 
diff --git a/tests/platform_independent/data/licenses.txt b/tests/platform_independent/data/licenses.txt
index 17d22c8..64c39a4 100644
--- a/tests/platform_independent/data/licenses.txt
+++ b/tests/platform_independent/data/licenses.txt
@@ -811,4 +811,4 @@ UNKNOWN
 UNKNOWN
 UNKNOWN
 UNKNOWN
-UNKNOWN
+UNKNOWN
\ No newline at end of file
diff --git a/tests/platform_independent/data/simple.json b/tests/platform_independent/data/simple.json
index 8b3ab9f..27c6226 100644
--- a/tests/platform_independent/data/simple.json
+++ b/tests/platform_independent/data/simple.json
@@ -18,4 +18,4 @@
 			"namever": "example-UNKNOWN"
 		}
 	]
-}
+}
\ No newline at end of file
diff --git a/tests/platform_independent/data/simple.md b/tests/platform_independent/data/simple.md
index 916e219..427a49c 100644
--- a/tests/platform_independent/data/simple.md
+++ b/tests/platform_independent/data/simple.md
@@ -1,8 +1,8 @@
 ## Info
 
-- **program**: licensecheck
-- **version**: dev
-- **license**: MIT LICENSE
+- program: licensecheck
+- version: dev
+- license: MIT LICENSE
 
 ## Project License
 
diff --git a/tests/platform_independent/data/simple.txt b/tests/platform_independent/data/simple.txt
index 3aae327..8c75067 100644
--- a/tests/platform_independent/data/simple.txt
+++ b/tests/platform_independent/data/simple.txt
@@ -1,5 +1,5 @@
-
-               Info
+                                  
+               Info               
 ┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
 ┃ Item            ┃ Value        ┃
 ┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
@@ -8,8 +8,8 @@
 │ license         │ MIT LICENSE  │
 │ project_license │ MIT LICENSE  │
 └─────────────────┴──────────────┘
-
-          List Of Packages
+                                     
+          List Of Packages           
 ┏━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━┓
 ┃ Compatible ┃ Package ┃ License(s) ┃
 ┡━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━┩
diff --git a/tests/platform_independent/test_formatter.py b/tests/platform_independent/test_formatter.py
index 0b578fb..faeace1 100644
--- a/tests/platform_independent/test_formatter.py
+++ b/tests/platform_independent/test_formatter.py
@@ -1,9 +1,12 @@
+from __future__ import annotations
 from pathlib import Path
 
-from licensecheck import formatter
+from licensecheck import fmt
 from licensecheck.types import License, PackageInfo, ucstr
+import pytest
+from pathlib import Path
 
-formatter.INFO = {"program": "licensecheck", "version": "dev", "license": "MIT LICENSE"}
+fmt.INFO = {"program": "licensecheck", "version": "dev", "license": "MIT LICENSE"}
 
 THISDIR = str(Path(__file__).resolve().parent)
 
@@ -34,85 +37,47 @@ complexPackages = [
 myLice = License.MIT
 
 
-def test_simpleMarkdown() -> None:
-	fmt = formatter.markdown(myLice, simplePackages)
-	# Path(f"{THISDIR}/data/simple.md").write_text(fmt, "utf-8")
-	assert fmt == Path(f"{THISDIR}/data/simple.md").read_text("utf-8")
-
-
-def test_advancedMarkdown() -> None:
-	fmt = formatter.markdown(myLice, complexPackages)
-	# Path(f"{THISDIR}/data/advanced.md").write_text(fmt, "utf-8")
-	assert fmt == Path(f"{THISDIR}/data/advanced.md").read_text("utf-8")
-
-
-def test_advancedMarkdownIgnoreParams() -> None:
-	fmt = formatter.markdown(myLice, complexPackages)
-	# Path(f"{THISDIR}/data/advanced_ignore_params.md").write_text(fmt, "utf-8")
-	assert fmt == Path(f"{THISDIR}/data/advanced_ignore_params.md").read_text("utf-8")
-
-
-def test_simpleRaw() -> None:
-	fmt = formatter.raw(myLice, simplePackages)
-	# Path(f"{THISDIR}/data/simple.json").write_text(fmt, "utf-8")
-	assert fmt == Path(f"{THISDIR}/data/simple.json").read_text("utf-8")
-
-
-def test_advancedRaw() -> None:
-	fmt = formatter.raw(myLice, complexPackages)
-	# Path(f"{THISDIR}/data/advanced.json").write_text(fmt, "utf-8")
-	assert fmt == Path(f"{THISDIR}/data/advanced.json").read_text("utf-8")
-
-
-def test_advancedRawIgnoreParams() -> None:
-	fmt = formatter.raw(
-		myLice, complexPackages, hide_parameters=[ucstr("HOMEPAGE"), ucstr("AUTHOR")]
-	)
-	# Path(f"{THISDIR}/data/advanced_ignore_params.json").write_text(fmt, "utf-8")
-	assert fmt == Path(f"{THISDIR}/data/advanced_ignore_params.json").read_text("utf-8")
-
-
-def test_simpleRawCsv() -> None:
-	fmt = formatter.rawCsv(myLice, simplePackages)
-	# Path(f"{THISDIR}/data/simple.csv").write_text(fmt, "utf-8")
-	assert fmt == Path(f"{THISDIR}/data/simple.csv").read_text("utf-8")
-
-
-def test_advancedRawCsv() -> None:
-	fmt = formatter.rawCsv(myLice, complexPackages)
-	# Path(f"{THISDIR}/data/advanced.csv").write_text(fmt, "utf-8")
-	assert fmt == Path(f"{THISDIR}/data/advanced.csv").read_text("utf-8")
-
-
-def test_simpleAnsi() -> None:
-	fmt = formatter.ansi(myLice, simplePackages)
-	# Path(f"{THISDIR}/data/simple.ansi").write_text(fmt, "utf-8")
-	assert fmt == Path(f"{THISDIR}/data/simple.ansi").read_text("utf-8")
-
-
-def test_advancedAnsi() -> None:
-	fmt = formatter.ansi(myLice, complexPackages)
-	# Path(f"{THISDIR}/data/advanced.ansi").write_text(fmt, "utf-8")
-	assert fmt == Path(f"{THISDIR}/data/advanced.ansi").read_text("utf-8")
-
-
-def test_advancedAnsiIgnoreParams() -> None:
-	fmt = formatter.ansi(myLice, complexPackages, hide_parameters=[])
-	assert fmt == Path(f"{THISDIR}/data/advanced.ansi").read_text("utf-8")
-
-
-def test_simplePlainText() -> None:
-	fmt = formatter.plainText(myLice, simplePackages)
-	# Path(f"{THISDIR}/data/simple.txt").write_text(fmt, "utf-8")
-	assert fmt == Path(f"{THISDIR}/data/simple.txt").read_text("utf-8")
-
-
-def test_advancedPlainText() -> None:
-	fmt = formatter.plainText(myLice, complexPackages)
-	# Path(f"{THISDIR}/data/advanced.txt").write_text(fmt, "utf-8")
-	assert fmt == Path(f"{THISDIR}/data/advanced.txt").read_text("utf-8")
-
-
-def test_advancedPlainTextIgnoreParams() -> None:
-	fmt = formatter.plainText(myLice, complexPackages, hide_parameters=[ucstr("WRONG_PARAMETER")])
-	assert fmt == Path(f"{THISDIR}/data/advanced.txt").read_text("utf-8")
+@pytest.mark.parametrize(
+	"_fmt, packages, expected_output_file, hide_params",
+	[
+		("markdown", simplePackages, "simple.md", None),
+		("markdown", complexPackages, "advanced.md", None),
+		("markdown", complexPackages, "advanced_ignore_params.md", []),
+		("json", simplePackages, "simple.json", None),
+		("json", complexPackages, "advanced.json", None),
+		(
+			"json",
+			complexPackages,
+			"advanced_ignore_params.json",
+			[ucstr("HOMEPAGE"), ucstr("AUTHOR")],
+		),
+		("csv", simplePackages, "simple.csv", None),
+		("csv", complexPackages, "advanced.csv", None),
+		("ansi", simplePackages, "simple.ansi", None),
+		("ansi", complexPackages, "advanced.ansi", None),
+		("ansi", complexPackages, "advanced.ansi", []),
+		("simple", simplePackages, "simple.txt", None),
+		("simple", complexPackages, "advanced.txt", None),
+		("simple", complexPackages, "advanced.txt", [ucstr("WRONG_PARAMETER")]),
+	],
+)
+def test_output__fmt(
+	_fmt: str,
+	packages: list[PackageInfo],
+	expected_output_file: str,
+	hide_params: list[ucstr] | None,
+):
+	actual_output = fmt.fmt(_fmt, myLice, packages, hide_parameters=hide_params)
+	expected_output = Path(f"{THISDIR}/data/{expected_output_file}")
+	# expected_output.write_text(actual_output, "utf-8")
+	assert assert_eq(actual_output, expected_output.read_text("utf-8"))
+
+
+def assert_eq(actual_input: str, expected_output: str):
+	actual = actual_input.strip().splitlines()
+	expected = expected_output.strip().splitlines()
+
+	if len(expected) != len(actual):
+		return False
+
+	return [x.strip() for x in actual] == [x.strip() for x in expected]
diff --git a/tests/platform_independent/test_packageinfo.py b/tests/platform_independent/test_packageinfo.py
index 5d10ea8..b4b52bd 100644
--- a/tests/platform_independent/test_packageinfo.py
+++ b/tests/platform_independent/test_packageinfo.py
@@ -5,21 +5,24 @@ from typing import Any
 
 import pytest
 
-from licensecheck import packageinfo, types
+from licensecheck import types, packageinfo
+from licensecheck.packageinfo import PackageInfoManager
 
 THISDIR = str(Path(__file__).resolve().parent)
 
 
+package_info_manager = PackageInfoManager("https://pypi.org/pypi/")
+
 def test_getPackageInfoLocal() -> None:
 	try:
-		package = packageinfo.getPackageInfoLocal(types.ucstr("requests"))
+		package = package_info_manager.getPackageInfoLocal(types.ucstr("requests"))
 		assert package.name == "requests"
 	except ModuleNotFoundError:
 		assert True
 
 
 def test_getPackageInfoPypi() -> None:
-	package = packageinfo.getPackageInfoPypi(types.ucstr("requests"))
+	package = package_info_manager.getPackageInfoPypi(types.ucstr("requests"))
 
 	assert package.name == "requests"
 	assert package.homePage == "https://requests.readthedocs.io"
@@ -29,7 +32,7 @@ def test_getPackageInfoPypi() -> None:
 
 def test_getPackageInfoLocalNotFound() -> None:
 	try:
-		packageinfo.getPackageInfoLocal(types.ucstr("this_package_does_not_exist"))
+		package_info_manager.getPackageInfoLocal(types.ucstr("this_package_does_not_exist"))
 		raise AssertionError
 	except ModuleNotFoundError:
 		assert True
@@ -37,14 +40,14 @@ def test_getPackageInfoLocalNotFound() -> None:
 
 def test_getPackagePypiLocalNotFound() -> None:
 	try:
-		packageinfo.getPackageInfoPypi(types.ucstr("this_package_does_not_exist"))
+		package_info_manager.getPackageInfoPypi(types.ucstr("this_package_does_not_exist"))
 		raise AssertionError
 	except ModuleNotFoundError:
 		assert True
 
 
 def test_getPackages() -> None:
-	packages = packageinfo.getPackages({types.ucstr("requests")})
+	packages = package_info_manager.getPackages({types.ucstr("requests")})
 
 	assert all(
 		(
@@ -58,7 +61,7 @@ def test_getPackages() -> None:
 
 
 def test_getPackagesNotFound() -> None:
-	packages = packageinfo.getPackages({types.ucstr("this_package_does_not_exist")})
+	packages = package_info_manager.getPackages({types.ucstr("this_package_does_not_exist")})
 
 	assert all(
 		(package.name == "THIS_PACKAGE_DOES_NOT_EXIST" and package.errorCode == 1)