diff --git a/.gitignore b/.gitignore index 2620d7f..5f20c23 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ screenshots # Python __pycache__ *.pyc +venv # IDE .idea diff --git a/pandasgui/constants.py b/pandasgui/constants.py index 8ba6f55..6f2e3bc 100644 --- a/pandasgui/constants.py +++ b/pandasgui/constants.py @@ -1,8 +1,18 @@ import os +import sys from appdirs import user_data_dir +import pkg_resources LOCAL_DATA_DIR = os.path.join(user_data_dir(), "pandasgui") LOCAL_DATASET_DIR = os.path.join(LOCAL_DATA_DIR, "dataset_files") os.makedirs(LOCAL_DATA_DIR, exist_ok=True) os.makedirs(LOCAL_DATASET_DIR, exist_ok=True) + +PANDASGUI_ICON_PATH = pkg_resources.resource_filename(__name__, "resources/images/icon.png") +PANDASGUI_ICON_PATH_ICO = pkg_resources.resource_filename(__name__, "resources/images/icon.ico") + +if sys.platform == "win32": + SHORTCUT_PATH = os.path.join(os.getenv('APPDATA'), 'Microsoft/Windows/Start Menu/Programs/PandasGUI.lnk', ) + PY_INTERPRETTER_PATH = os.path.join(os.path.dirname(sys.executable), 'python.exe') + PYW_INTERPRETTER_PATH = os.path.join(os.path.dirname(sys.executable), 'pythonw.exe') \ No newline at end of file diff --git a/pandasgui/datasets.py b/pandasgui/datasets.py index b363ef3..2a12a84 100644 --- a/pandasgui/datasets.py +++ b/pandasgui/datasets.py @@ -171,7 +171,7 @@ def __getattr__(name: str) -> Union[pd.DataFrame, Dict[str, pd.DataFrame]]: ################ # These just improve intellisense since the type hint on the __getattr__ return value didn't work -all_datasets: pd.DataFrame +all_datasets: Dict[str, pd.DataFrame] pokemon: pd.DataFrame googleplaystore: pd.DataFrame googleplaystore_reviews: pd.DataFrame diff --git a/pandasgui/gui.py b/pandasgui/gui.py index 3b85b2b..d1c233f 100644 --- a/pandasgui/gui.py +++ b/pandasgui/gui.py @@ -1,16 +1,17 @@ import inspect +import io import os import sys import pprint from typing import Callable, Union from dataclasses import dataclass import pandas as pd -import pkg_resources from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import Qt import pandasgui -from pandasgui.store import PandasGuiStore +from pandasgui.constants import PANDASGUI_ICON_PATH +from pandasgui.store import PandasGuiStore, SettingsSchema from pandasgui.utility import as_dict, fix_ipython, get_figure_type, resize_widget from pandasgui.widgets.find_toolbar import FindToolbar from pandasgui.widgets.json_viewer import JsonViewer @@ -42,7 +43,7 @@ def except_hook(cls, exception, traceback): class PandasGui(QtWidgets.QMainWindow): - def __init__(self, settings: dict = {}, **kwargs): + def __init__(self, settings: SettingsSchema = {}, **kwargs): """ Args: settings: Dict of settings, as defined in pandasgui.store.SettingsStore @@ -70,41 +71,44 @@ def __init__(self, settings: dict = {}, **kwargs): # Create all widgets self.init_ui() - plotly_kwargs = {key: value for (key, value) in kwargs.items() if get_figure_type(value) is not None} - json_kwargs = {key: value for (key, value) in kwargs.items() if any([ - issubclass(type(value), list), - issubclass(type(value), dict), - ])} - dataframe_kwargs = {key: value for (key, value) in kwargs.items() if any([ - issubclass(type(value), pd.DataFrame), - issubclass(type(value), pd.Series), - ])} - - if json_kwargs: - for name, val in json_kwargs.items(): - jv = JsonViewer(val) - jv.setWindowTitle(name) - self.store.add_item(jv, name) - - if plotly_kwargs: - for name, fig in plotly_kwargs.items(): - pv = FigureViewer(fig) - pv.setWindowTitle(name) - self.store.add_item(pv, name) - - if dataframe_kwargs: - # Adds DataFrames listed in kwargs to data store. - for df_name, df in dataframe_kwargs.items(): - self.store.add_dataframe(df, df_name) - + # Add the item to the store differently depending what type it is + for key, value in kwargs.items(): + # Pandas objects + if issubclass(type(value), pd.DataFrame) or issubclass(type(value), pd.Series): + self.store.add_dataframe(value, key) + # Spark + elif hasattr(value, 'toPandas'): + temp = value.toPandas() + self.store.add_dataframe(value, key) + elif hasattr(value, 'to_pandas'): + temp = value.to_pandas() + self.store.add_dataframe(value, key) + # JSON + elif issubclass(type(value), list) or issubclass(type(value), dict): + jv = JsonViewer(value) + jv.setWindowTitle(key) + self.store.add_item(jv, key) + # Graphs + elif get_figure_type(value) is not None: + pv = FigureViewer(value) + pv.setWindowTitle(key) + self.store.add_item(pv, key) + # File buffers + elif issubclass(type(value), io.BytesIO) or issubclass(type(value), io.StringIO): + value.seek(0) + df = pd.read_csv(value) + self.store.add_dataframe(df, key) + # File paths + elif issubclass(type(value), str): + if os.path.exists(value): + self.store.import_file(value) + else: + logger.warning(f"File path is invalid or does not exist: {value}") + else: + logger.warning(f"PandasGUI Unsupported type: {type(value)}") # Default to first item self.navigator.setCurrentItem(self.navigator.topLevelItem(0)) - self.show() - # Start event loop if blocking enabled - if self.store.settings.block.value: - self.app.exec_() - # Create and add all widgets to GUI. def init_ui(self): self.code_history_viewer = None @@ -122,8 +126,7 @@ def init_ui(self): # Set window title and icon self.setWindowTitle("PandasGUI") - pdgui_icon_path = pkg_resources.resource_filename(__name__, "resources/images/icon.png") - self.app.setWindowIcon(QtGui.QIcon(pdgui_icon_path)) + self.app.setWindowIcon(QtGui.QIcon(PANDASGUI_ICON_PATH)) # Hide the question mark on dialogs self.app.setAttribute(Qt.AA_DisableWindowContextHelpButton) @@ -177,27 +180,29 @@ class MenuItem: func: Callable shortcut: str = '' - items = {'Edit': [MenuItem(name='Find', - func=self.find_bar.show_find_bar, - shortcut='Ctrl+F'), - MenuItem(name='Copy', - func=self.copy, - shortcut='Ctrl+C'), - MenuItem(name='Copy With Headers', - func=self.copy_with_headers, - shortcut='Ctrl+Shift+C'), - MenuItem(name='Paste', - func=self.paste, - shortcut='Ctrl+V'), - MenuItem(name='Import', - func=self.import_dialog), - MenuItem(name='Import From Clipboard', - func=self.import_from_clipboard), - MenuItem(name='Export', - func=self.export_dialog), - MenuItem(name='Code Export', - func=self.show_code_export), - ], + items = {'Edit': [MenuItem(name='Find', + func=self.find_bar.show_find_bar, + shortcut='Ctrl+F'), + MenuItem(name='Copy', + func=self.copy, + shortcut='Ctrl+C'), + MenuItem(name='Copy With Headers', + func=self.copy_with_headers, + shortcut='Ctrl+Shift+C'), + MenuItem(name='Paste', + func=self.paste, + shortcut='Ctrl+V'), + MenuItem(name='Import', + func=self.import_dialog), + MenuItem(name='Import From Clipboard', + func=self.import_from_clipboard), + MenuItem(name='Export', + func=self.export_dialog), + MenuItem(name='Export To Clipboard', + func=self.export_to_clipboard), + MenuItem(name='Code Export', + func=self.show_code_export), + ], 'DataFrame': [MenuItem(name='Delete Selected DataFrames', func=self.delete_selected_dataframes), MenuItem(name='Reload DataFrames', @@ -206,27 +211,33 @@ class MenuItem: MenuItem(name='Parse All Dates', func=lambda: self.store.selected_pgdf.parse_all_dates()), ], - 'Settings': [MenuItem(name='Preferences...', - func=self.edit_settings), - {"Context Menus": [MenuItem(name='Add PandasGUI To Context Menu', - func=self.add_to_context_menu), - MenuItem(name='Remove PandasGUI From Context Menu', - func=self.remove_from_context_menu), - MenuItem(name='Add JupyterLab To Context Menu', - func=self.add_jupyter_to_context_menu), - MenuItem(name='Remove JupyterLab From Context Menu', - func=self.remove_jupyter_from_context_menu), ]} - - ], - 'Debug': [MenuItem(name='About', - func=self.about), - MenuItem(name='Browse Sample Datasets', - func=self.show_sample_datasets), - MenuItem(name='View PandasGuiStore', - func=self.view_store), - MenuItem(name='View DataFrame History', - func=self.view_history), - ] + 'Settings': [MenuItem(name='Preferences...', + func=self.edit_settings), + {"Context Menus": [MenuItem(name='Add PandasGUI To Context Menu', + func=self.add_to_context_menu), + MenuItem(name='Remove PandasGUI From Context Menu', + func=self.remove_from_context_menu), + MenuItem(name='Add PandasGUI To Start Menu', + func=self.add_to_start_menu), + MenuItem(name='Remove PandasGUI From Start Menu', + func=self.remove_from_start_menu), + MenuItem(name='Add JupyterLab To Context Menu', + func=self.add_jupyter_to_context_menu), + MenuItem(name='Remove JupyterLab From Context Menu', + func=self.remove_jupyter_from_context_menu), ]} + + ], + 'Debug': [MenuItem(name='About', + func=self.about), + MenuItem(name='Browse Sample Datasets', + func=self.show_sample_datasets), + MenuItem(name='View PandasGuiStore', + func=self.view_store), + MenuItem(name='View DataFrame History', + func=self.view_history), + MenuItem(name='Throw Error', + func=lambda x: exec('raise(Exception("Exception raised by PandasGUI"))')), + ] } def add_menus(dic, root): @@ -278,7 +289,6 @@ def show_code_export(self): def update_code_export(self): self.store.selected_pgdf.dataframe_explorer.code_history_viewer.refresh() - def delete_selected_dataframes(self): for name in [item.text(0) for item in self.navigator.selectedItems()]: self.store.remove_dataframe(name) @@ -347,6 +357,9 @@ def import_from_clipboard(self): skip_blank_lines=False) self.store.add_dataframe(df) + def export_to_clipboard(self): + self.store.selected_pgdf.df.to_clipboard(excel=True, index=True) + # https://stackoverflow.com/a/29769228/3620725 def add_to_context_menu(self): import winreg @@ -367,6 +380,28 @@ def remove_from_context_menu(self): winreg.DeleteKey(key, "Software\Classes\*\shell\Open with PandasGUI\command") winreg.DeleteKey(key, "Software\Classes\*\shell\Open with PandasGUI") + # https://stackoverflow.com/a/46081847 + def add_to_start_menu(self): + if sys.platform == "win32": + import os + import win32com.client + import pythoncom + from pandasgui.constants import PANDASGUI_ICON_PATH_ICO, PYW_INTERPRETTER_PATH, SHORTCUT_PATH + + shell = win32com.client.Dispatch("WScript.Shell") + shortcut = shell.CreateShortCut(SHORTCUT_PATH) + shortcut.Targetpath = PYW_INTERPRETTER_PATH + shortcut.Arguments = '-c "import pandasgui; pandasgui.show()"' + shortcut.IconLocation = PANDASGUI_ICON_PATH_ICO + shortcut.WindowStyle = 7 # 7 - Minimized, 3 - Maximized, 1 - Normal + shortcut.save() + + def remove_from_start_menu(self): + if sys.platform == "win32": + from pandasgui.constants import SHORTCUT_PATH + import os + os.remove(SHORTCUT_PATH) + def add_jupyter_to_context_menu(self): import winreg @@ -436,13 +471,13 @@ def reload_data(self): def show(*args, - settings={}, + settings: SettingsSchema = {}, **kwargs): ''' Objects provided as args and kwargs should be any of the following: DataFrame Show it using PandasGui Series Show it using PandasGui - Figure Show it using FigureViewer. Supports figures from plotly, bokeh, matplotlib, altair + Figure Show it using FigureViewer. Supports figures from plotly, bokeh, matplotlib, altair, PIL dict/list Show it using JsonViewer ''' logger.info("Opening PandasGUI") @@ -487,6 +522,13 @@ def pg(line): else: raise e + pandas_gui.show() + pandas_gui.activateWindow() + pandas_gui.raise_() + + # Start event loop if blocking enabled + if pandas_gui.store.settings.block.value: + pandas_gui.app.exec_() return pandas_gui diff --git a/pandasgui/jotly.py b/pandasgui/jotly.py index 64d1f72..cc8d862 100644 --- a/pandasgui/jotly.py +++ b/pandasgui/jotly.py @@ -32,7 +32,7 @@ class OtherDataFrame(DataFrame): # Graphing def histogram(data_frame: DataFrame, - x: ColumnName = None, + x: ColumnNameList = None, color: ColumnName = None, facet_row: ColumnName = None, facet_col: ColumnName = None, @@ -46,6 +46,7 @@ def histogram(data_frame: DataFrame, facet_col=facet_col, marginal=marginal, cumulative=cumulative, + # nbins=nbins, **kwargs) return fig @@ -85,7 +86,7 @@ def scatter(data_frame: DataFrame, def line(data_frame: DataFrame, - y: ColumnName = None, + y: ColumnNameList = None, x: ColumnName = None, color: ColumnName = None, facet_row: ColumnName = None, @@ -120,7 +121,7 @@ def line(data_frame: DataFrame, def bar(data_frame: DataFrame, - y: ColumnName = None, + y: ColumnNameList = None, x: ColumnName = None, color: ColumnName = None, facet_row: ColumnName = None, @@ -156,7 +157,7 @@ def bar(data_frame: DataFrame, def box(data_frame: DataFrame, - y: ColumnName = None, + y: ColumnNameList = None, x: ColumnName = None, color: ColumnName = None, facet_row: ColumnName = None, @@ -175,7 +176,7 @@ def box(data_frame: DataFrame, def violin(data_frame: DataFrame, - y: ColumnName = None, + y: ColumnNameList = None, x: ColumnName = None, color: ColumnName = None, facet_row: ColumnName = None, diff --git a/pandasgui/store.py b/pandasgui/store.py index 0c5f5b3..46cdd51 100644 --- a/pandasgui/store.py +++ b/pandasgui/store.py @@ -11,7 +11,7 @@ from dataclasses import dataclass, field from typing import Iterable, List, Union -from typing_extensions import Literal +from typing_extensions import Literal, TypedDict import pandas as pd from pandas import DataFrame from PyQt5 import QtCore, QtWidgets @@ -83,19 +83,30 @@ def __setattr__(self, key, value): super().__setattr__(key, value) -DEFAULT_SETTINGS = {'editable': True, - 'block': None, - 'theme': 'light', - 'auto_finish': True, +DEFAULT_SETTINGS = {'editable': True, + 'block': None, + 'theme': 'light', + 'auto_finish': True, 'refresh_statistics': True, - 'render_mode': 'auto', - 'aggregation': 'mean', - 'title_format': "{name}: {title_columns}{title_dimensions}{names}{title_y}{title_z}{over_by}" - "{title_x} {selection}
{groupings}{filters} {title_trendline}" + 'render_mode': 'auto', + 'aggregation': 'mean', + 'title_format': "{name}: {title_columns}{title_dimensions}{names}{title_y}{title_z}{over_by}" + "{title_x} {selection}
{groupings}{filters} {title_trendline}" } +class SettingsSchema(TypedDict): + block: bool + editable: bool + theme: Literal['light', 'dark', 'classic'] + refresh_statistics: bool + auto_finish: bool + render_mode: Literal['auto', 'webgl', 'svg'] + aggregation: Literal['mean', 'median', 'min', 'max', 'sum', None] + title_format: str + + @dataclass class SettingsStore(DictLike, QtCore.QObject): settingsChanged = QtCore.pyqtSignal() @@ -103,6 +114,7 @@ class SettingsStore(DictLike, QtCore.QObject): block: Setting editable: Setting theme: Setting + refresh_statistics: Setting auto_finish: Setting render_mode: Setting aggregation: Setting @@ -175,7 +187,7 @@ def __init__(self, **settings): self.title_format = Setting(label="title_format", value=settings['title_format'], description="title_format", - dtype=dict, + dtype=str, persist=True) def reset_to_defaults(self): @@ -330,13 +342,13 @@ def refresh_statistics(self, force=False): if force or self.settings.refresh_statistics.value: df = self.df self.column_statistics = pd.DataFrame({ - "Type": df.dtypes.astype(str), - "Count": df.count(), + "Type": df.dtypes.astype(str), + "Count": df.count(), "N Unique": nunique(df), - "Mean": df.mean(numeric_only=True), - "StdDev": df.std(numeric_only=True), - "Min": df.min(numeric_only=True), - "Max": df.max(numeric_only=True), + "Mean": df.mean(numeric_only=True), + "StdDev": df.std(numeric_only=True), + "Min": df.min(numeric_only=True), + "Max": df.max(numeric_only=True), }, index=df.columns ) @@ -606,7 +618,7 @@ def apply_filters(self): for ix, filt in enumerate(self.filters): if filt.enabled and not filt.failed: try: - df = df.query(filt.expr) + df = df.query(filt.expr, engine='python') # Handle case where filter returns only one row if isinstance(df, pd.Series): df = df.to_frame().T @@ -826,37 +838,43 @@ def add_dataframe(self, pgdf: Union[DataFrame, PandasGuiDataFrameStore], def remove_dataframe(self, name_or_index): self.remove_item(name_or_index) + # Handle import (including drag and drop) for all valid file types @status_message_decorator('Importing file "{path}"...') def import_file(self, path): + filename = os.path.basename(path) if not os.path.isfile(path): logger.warning("Path is not a file: " + path) elif path.endswith(".csv"): - filename = os.path.split(path)[1].split('.csv')[0] df = pd.read_csv(path, engine='python') self.add_dataframe(df, filename) elif path.endswith(".xlsx"): - filename = os.path.split(path)[1].split('.csv')[0] df_dict = pd.read_excel(path, sheet_name=None) for sheet_name in df_dict.keys(): df_name = f"{filename} - {sheet_name}" self.add_dataframe(df_dict[sheet_name], df_name) elif path.endswith(".parquet"): - filename = os.path.split(path)[1].split('.parquet')[0] df = pd.read_parquet(path, engine='pyarrow') self.add_dataframe(df, filename) elif path.endswith(".json"): - filename = os.path.split(path)[1].split('.json')[0] with open(path) as f: data = json.load(f) from pandasgui.widgets.json_viewer import JsonViewer jv = JsonViewer(data) self.add_item(jv, filename) elif path.endswith(".pkl"): - filename = os.path.split(path)[1].split('.pkl')[0] df = pd.read_pickle(path) self.add_dataframe(df, filename) + elif any([path.endswith(x) for x in [".sqlite", ".sqlite3", ".db", ".db3"]]): + import sqlite3 + with sqlite3.connect(path) as dbcon: + tables = list(pd.read_sql_query("SELECT name FROM sqlite_master WHERE type='table';", dbcon)['name']) + out = {tbl: pd.read_sql_query(f"SELECT * from {tbl}", dbcon) for tbl in tables} + for key, val in out.items(): + self.add_dataframe(val, f"{filename}-{key}") + else: - logger.warning("Can only import csv / xlsx / parquet. Invalid file: " + path) + logger.warning(f"Invalid file: {path}\n" + "Can only import the following file types: csv, xlsx, parquet, json, pkl, sqlite") def get_dataframes(self, names: Union[None, str, list, int] = None): if type(names) == str: diff --git a/pandasgui/utility.py b/pandasgui/utility.py index d6a7cce..2c15085 100644 --- a/pandasgui/utility.py +++ b/pandasgui/utility.py @@ -1,7 +1,9 @@ import logging +import typing + import pandas as pd from PyQt5 import QtWidgets -from typing import List, Union +from typing import List, Union, Literal import sys import inspect from collections import OrderedDict @@ -310,6 +312,25 @@ def clean_dataframe(df, name="DataFrame"): return df +def move_columns(df: pd.DataFrame, + names: Union[str, List[str]], + to: Union[int, Literal['start', 'end']]): + if to == 'start': + to = 0 + elif to == 'end': + to = len(df.columns) - 1 + + if type(names) == str: + names = [names] + + cols = df.columns + cols_exc_names = [c for c in cols if c not in names] + new_cols = cols_exc_names[:to] + names + cols_exc_names[to:] + df = df.reindex(new_cols, axis=1) + + return df + + def test_logging(): logger.debug("debug") logger.info("info") @@ -424,7 +445,13 @@ def refactor_variable(expr, old_name, new_name): return astor.code_gen.to_source(tree) -def get_figure_type(fig): +def get_figure_type(fig) -> typing.Literal[ + "plotly", + "matplotlib", + "bokeh", + "altair", + "PIL", +]: # Plotly try: import plotly.basedatatypes @@ -458,6 +485,13 @@ def get_figure_type(fig): return "altair" except ModuleNotFoundError: pass + # PIL + try: + import PIL.Image + if issubclass(type(fig), PIL.Image.Image): + return "PIL" + except ModuleNotFoundError: + pass return None diff --git a/pandasgui/widgets/figure_viewer.py b/pandasgui/widgets/figure_viewer.py index c0d6521..c45a140 100644 --- a/pandasgui/widgets/figure_viewer.py +++ b/pandasgui/widgets/figure_viewer.py @@ -46,7 +46,7 @@ def __init__(self, fig=None, store=None): self.settings().setAttribute(PyQt5.QtWebEngineWidgets.QWebEngineSettings.WebGLEnabled, True) # https://stackoverflow.com/a/8577226/3620725 - self.temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) + self.temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding='utf-8') self.set_figure(fig) self.setWindowTitle("Plotly Viewer") @@ -85,7 +85,13 @@ def set_figure(self, fig=None): tmp = StringIO() fig.save(tmp, format='html') html = tmp.getvalue() - + elif fig_type == "PIL": + import base64 + from io import BytesIO + tmpfile = BytesIO() + fig.save(tmpfile, format='png') + encoded = base64.b64encode(tmpfile.getvalue()).decode('utf-8') + html = ''.format(encoded) else: raise TypeError diff --git a/pandasgui/widgets/func_ui.py b/pandasgui/widgets/func_ui.py index 4e01689..dcb7eeb 100644 --- a/pandasgui/widgets/func_ui.py +++ b/pandasgui/widgets/func_ui.py @@ -77,6 +77,19 @@ def __init__(self, arg_name, default_value=None): self.default_value = default_value +@dataclass +class IntArg: + arg_name: str + default_value: int + + def __init__(self, arg_name, default_value=None): + if default_value is None: + default_value = None + + self.arg_name = arg_name + self.default_value = default_value + + # Display a dropdown to pick a DataFrame name from the list of DataFrames in the GUI @dataclass class OtherDataFrameArg: @@ -123,6 +136,9 @@ def __init__(self, elif arg_type == bool: args.append(BooleanArg(arg_name, default_value=arg_default)) + elif arg_type == int: + args.append(IntArg(arg_name, default_value=arg_default)) + elif arg_type == OtherDataFrame: args.append(OtherDataFrameArg(arg_name, default_value=arg_default)) @@ -133,7 +149,7 @@ def __init__(self, self.icon_path = icon_path name: str - args: List[Union[ColumnNameArg, ColumnNameListArg, LiteralArg, BooleanArg]] + args: List[Union[ColumnNameArg, ColumnNameListArg, LiteralArg, BooleanArg, IntArg]] label: str function: Callable icon_path: str @@ -152,6 +168,7 @@ def __init__(self, pgdf: PandasGuiDataFrameStore): self.tree.setDragDropMode(self.tree.DragOnly) self.tree.setHeaderLabels(['Name', '#Unique', 'Type']) + class FuncUi(QtWidgets.QWidget): valuesChanged = QtCore.pyqtSignal() itemDropped = QtCore.pyqtSignal() @@ -447,6 +464,25 @@ def set_schema(self, schema: Schema): checkbox.stateChanged.emit(Qt.Checked if val else Qt.Unchecked) checkbox.stateChanged.connect(lambda: self.valuesChanged.emit()) + + elif type(arg) == IntArg: + spinbox = QtWidgets.QSpinBox() + + spinbox.valueChanged.connect( + lambda val, item=item: item.setData(1, Qt.UserRole, val)) + + item.treeWidget().setItemWidget(item, 1, spinbox) + if arg.arg_name in self.remembered_values.keys(): + val = self.remembered_values[arg.arg_name] + elif arg.arg_name in asdict(SETTINGS_STORE).keys(): + val = SETTINGS_STORE[arg.arg_name].value + else: + val = arg.default_value + + spinbox.setValue(val) + spinbox.stateChanged.emit(val) + spinbox.stateChanged.connect(lambda: self.valuesChanged.emit()) + elif type(arg) == LiteralArg: combo_box = QtWidgets.QComboBox() combo_box.addItems([str(x) for x in arg.values]) diff --git a/setup.py b/setup.py index e6de155..028ca0c 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="pandasgui", - version="0.2.13", + version="0.2.15", description="A GUI for Pandas DataFrames.", author="Adam Rose", author_email="adrotog@gmail.com", @@ -16,6 +16,7 @@ long_description_content_type="text/markdown", exclude_package_data={'': ['.gitignore']}, # Using this instead of MANIFEST.in - https://pypi.org/project/setuptools-git/ + python_requires=">=3.7", setup_requires=['setuptools-git'], install_requires=[ "pandas", diff --git a/tests/tests.py b/tests/tests.py index fae7721..1160011 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -14,7 +14,7 @@ def generate_int_data(rows, cols): def generate_data(): - small_int_path = os.path.join("small_int.csv") + small_int_path = "small_int.csv" large_int_path = "large_int.csv" small_str_path = "small_str.csv" large_str_path = "large_str.csv" @@ -37,9 +37,7 @@ def getObjects(cls): return objects # string_data = generate_string_data(rows, cols) - int_data = pd.read_csv(large_int_path) - - # %% + int_data = pd.read_csv(generate_int_data(5000000, 50)) for i in range(10): df = pd.DataFrame(int_data) @@ -72,9 +70,9 @@ def test_webengine_import(): y=np.random.rand(100), mode="markers", marker={ - "size": 30, - "color": np.random.rand(100), - "opacity": 0.6, + "size": 30, + "color": np.random.rand(100), + "opacity": 0.6, "colorscale": "Viridis", }, ) @@ -92,10 +90,10 @@ def test_inputs(): df = pd.DataFrame(np.random.randn(6, 6), index=ix[:6], columns=ix[:6]) df2 = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6], - 2: [1, 2, 3], 'c': [4, 5, 6], }).rename(columns={'c': 2}) + 2: [1, 2, 3], 'c': [4, 5, 6], }).rename(columns={'c': 2}) - from pandasgui.datasets import all_datasets - show(df, df2, **all_datasets, settings={'block': False}) + from pandasgui.datasets import pokemon, googleplaystore + show(df, df2, pokemon, googleplaystore, settings={'block': False}) def test_code_history(): @@ -126,19 +124,55 @@ def test_code_history(): def test_json(): import requests from pandasgui import show - from pandasgui.datasets import all_datasets + from pandasgui.datasets import pokemon, googleplaystore comments = requests.get('https://jsonplaceholder.typicode.com/comments').json() photos = requests.get('https://jsonplaceholder.typicode.com/photos').json() - gui = show(comments, photos, **all_datasets, settings={'block': False}) + gui = show(comments, photos, pokemon, googleplaystore, settings={'block': False}) + + +def test_object_types(): + import requests + import pandas as pd + from pandasgui import show + from pandasgui.datasets import pokemon + import numpy as np + import plotly.graph_objs as go + import io + import tempfile + + fig = go.Figure() + fig.add_scatter( + x=np.random.rand(100), + y=np.random.rand(100), + mode="markers", + marker={ + "size": 30, + "color": np.random.rand(100), + "opacity": 0.6, + "colorscale": "Viridis", + }, + ) + + comments = requests.get('https://jsonplaceholder.typicode.com/comments').json() + + poke_sbuf = io.StringIO() + pokemon.head(10).to_csv(poke_sbuf) + poke_bbuf = io.BytesIO() + pokemon.head(10).to_csv(poke_bbuf) + poke_tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) + pokemon.head(10).to_csv(poke_tmp.name) + gui = show(comments, pokemon, fig, poke_sbuf, poke_bbuf, poke_tmp.name) +print('test_json') test_json() +print('test_inputs') test_inputs() +print('test_code_history') test_code_history() # test_webengine_import() -QtWidgets.QApplication # iterables = [["bar", "baz", "baz"], ["one", "two"]] # ix = pd.MultiIndex.from_product(iterables, names=["first", "second"]) # df = pd.DataFrame(np.random.randn(6, 6), index=ix[:6], columns=ix[:6])