Skip to content

Commit

Permalink
Retain file permissions for executable (#361)
Browse files Browse the repository at this point in the history
  • Loading branch information
WillAyd authored Oct 8, 2024
1 parent d75085a commit 9bb11de
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 54 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,24 @@ jobs:
env:
MACOSX_DEPLOYMENT_TARGET: "10.14"
CMAKE_APPLE_SILICON_PROCESSOR: ${{ matrix.os == 'macos-14' && 'arm64' || '' }}

test_no_tableauhyperapi:
name: Test wheels without tableauhyperapi on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, windows-2022, macos-12, macos-14]

steps:
- uses: actions/checkout@v4

# Used to host cibuildwheel
- uses: actions/setup-python@v3

- name: Build wheels for ${{ matrix.os }}
uses: pypa/cibuildwheel@v2.16.5
env:
CIBW_TEST_REQUIRES: pytest pytest-xdist[psutil] pandas>=2.0.0 polars narwhals
MACOSX_DEPLOYMENT_TARGET: "10.14"
CMAKE_APPLE_SILICON_PROCESSOR: ${{ matrix.os == 'macos-14' && 'arm64' || '' }}
4 changes: 3 additions & 1 deletion src/pantab/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ install(TARGETS libpantab
install(FILES ${tableauhyperapi-cxx_SOURCE_DIR}/${HYPERAPI_LIB_DIR}/${HYPERAPI_LIB_NAME}
DESTINATION ${SKBUILD_PROJECT_NAME}/)
install(DIRECTORY "${tableauhyperapi-cxx_SOURCE_DIR}/${HYPERAPI_BIN_LOC}/"
DESTINATION ${SKBUILD_PROJECT_NAME}/hyper)
DESTINATION ${SKBUILD_PROJECT_NAME}/hyper
USE_SOURCE_PERMISSIONS
)

# Support out of source builds
set(SRC_FILES
Expand Down
33 changes: 26 additions & 7 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
import polars as pl
import pyarrow as pa
import pytest
import tableauhyperapi as tab_api

try:
import tableauhyperapi as tab_api
except ModuleNotFoundError:
has_tableauhyperapi = False
else:
del tab_api
has_tableauhyperapi = True


def basic_arrow_table():
Expand Down Expand Up @@ -461,18 +468,30 @@ def table_mode(request):
return request.param


@pytest.fixture
def tab_api():
tab_api = pytest.importorskip("tableauhyperapi")
return tab_api


@pytest.fixture(
params=[
"table",
tab_api.Name("table"),
tab_api.TableName("table"),
tab_api.TableName("public", "table"),
tab_api.TableName("nonpublic", "table"),
(False, None, tuple(("table",))),
(True, "Name", tuple(("table",))),
(True, "TableName", tuple(("table",))),
(True, "TableName", ("public", "table")),
(True, "TableName", ("nonpublic", "table")),
]
)
def table_name(request):
"""Various ways to represent a table in Tableau."""
return request.param
needs_hyperapi, hyperapi_obj, args = request.param
if not needs_hyperapi:
return args[0]

tab_api = pytest.importorskip("tableauhyperapi")
obj = getattr(tab_api, hyperapi_obj)(*args)
return obj


@pytest.fixture
Expand Down
40 changes: 28 additions & 12 deletions tests/test_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import pandas as pd
import pandas.testing as tm
import pytest
import tableauhyperapi as tab_api

import pantab as pt

Expand All @@ -28,6 +27,7 @@ def test_read_doesnt_modify_existing_file(frame, tmp_hyper):
def test_reads_nullable_columns(tmp_hyper, compat):
# We don't ever write nullable columns but we should be able to read them
column_name = "int32"
tab_api = pytest.importorskip("tableauhyperapi")
table_name = tab_api.TableName("public", "table")
table = tab_api.TableDefinition(
table_name=table_name,
Expand Down Expand Up @@ -79,6 +79,7 @@ def test_read_query(frame, tmp_hyper):

def test_read_varchar(tmp_hyper):
column_name = "VARCHAR Column"
tab_api = pytest.importorskip("tableauhyperapi")
table_name = tab_api.TableName("public", "table")
table = tab_api.TableDefinition(
table_name=table_name,
Expand Down Expand Up @@ -116,6 +117,7 @@ def test_read_varchar(tmp_hyper):

def test_reader_handles_duplicate_columns(tmp_hyper):
column_name = "does_not_matter"
tab_api = pytest.importorskip("tableauhyperapi")
table_name = tab_api.TableName("public", "table")
table = tab_api.TableDefinition(
table_name=table_name,
Expand Down Expand Up @@ -187,20 +189,34 @@ def test_reader_invalid_process_params_raises(tmp_hyper):


@pytest.mark.parametrize(
"table_name",
"needs_hyperapi, hyperapi_obj, table_args",
[
"a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",
tab_api.Name("a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't"),
tab_api.TableName(
"public", "a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't"
(False, None, tuple(("a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",))),
(True, "Name", tuple(("a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",))),
(
True,
"TableName",
tuple(("a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",)),
),
tab_api.TableName(
"a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",
"a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",
(
True,
"TableName",
(
"a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",
"a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",
),
),
],
)
def test_reader_prevents_sql_injection(tmp_hyper, table_name):
def test_reader_prevents_sql_injection(
tmp_hyper, needs_hyperapi, hyperapi_obj, table_args
):
if not needs_hyperapi:
table = table_args[0]
else:
tab_api = pytest.importorskip("tableauhyperapi")
table = getattr(tab_api, hyperapi_obj)(*table_args)

frame = pd.DataFrame(list(range(10)), columns=["nums"]).astype("int8")
pt.frame_to_hyper(frame, tmp_hyper, table=table_name)
pt.frame_from_hyper(tmp_hyper, table=table_name)
pt.frame_to_hyper(frame, tmp_hyper, table=table)
pt.frame_from_hyper(tmp_hyper, table=table)
78 changes: 45 additions & 33 deletions tests/test_roundtrip.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import decimal
import sys

import pandas as pd
import pyarrow as pa
import pytest
import tableauhyperapi as tab_api

try:
import tableauhyperapi as tab_api
except ModuleNotFoundError:
has_tableauhyperapi = False
else:
del tab_api
has_tableauhyperapi = True

import pantab as pt

Expand Down Expand Up @@ -84,17 +90,27 @@ def test_multiple_tables(
if table_mode == "a":
expected = compat.concat_frames(expected, expected)

# some test trickery here
if not isinstance(table_name, tab_api.TableName) or table_name.schema_name is None:
table_name = tab_api.TableName("public", table_name)
if hasattr(table_name, "_unescaped_components"): # tableauhyperapi TableName
if table_name.schema_name:
exp_schema = table_name.schema_name.name.unescaped
else:
exp_schema = "public"

exp_table = table_name.name.unescaped
elif hasattr(table_name, "unescaped"): # tableauhyperapi Name
exp_schema = "public"
exp_table = table_name.unescaped
else:
exp_schema = "public"
exp_table = table_name

if isinstance(frame, pd.DataFrame) and return_type != "pandas":
expected = compat.drop_columns(expected, ["string_view", "binary_view"])

assert set(result.keys()) == set(
(
tuple(table_name._unescaped_components),
tuple(tab_api.TableName("public", "table2")._unescaped_components),
(exp_schema, exp_table),
("public", "table2"),
)
)
for val in result.values():
Expand Down Expand Up @@ -150,42 +166,38 @@ def test_empty_roundtrip(


@pytest.mark.parametrize(
"table_name",
"needs_hyperapi, hyperapi_obj, table_args",
[
"a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",
tab_api.Name("a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't"),
tab_api.TableName(
"public", "a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't"
(False, None, tuple(("a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",))),
(True, "Name", tuple(("a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",))),
(
True,
"TableName",
tuple(("a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",)),
),
tab_api.TableName(
"a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",
"a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",
(
True,
"TableName",
(
"a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",
"a';DROP TABLE users;DELETE FROM foo WHERE 't' = 't",
),
),
],
)
def test_write_prevents_injection(tmp_hyper, table_name):
def test_write_prevents_injection(tmp_hyper, needs_hyperapi, hyperapi_obj, table_args):
if not needs_hyperapi:
table = table_args[0]
else:
tab_api = pytest.importorskip("tableauhyperapi")
table = getattr(tab_api, hyperapi_obj)(*table_args)

frame = pd.DataFrame(list(range(10)), columns=["nums"]).astype("int8")
frames = {table_name: frame}
frames = {table: frame}
pt.frames_to_hyper(frames, tmp_hyper)
pt.frames_from_hyper(tmp_hyper)


def test_roundtrip_works_without_tableauhyperapi(frame, tmp_hyper, monkeypatch):
libname = "tableauhyperapi"
mods = set(sys.modules.keys())
for mod in mods:
if mod.startswith(libname):
monkeypatch.delitem(sys.modules, mod)

pt.frame_to_hyper(
frame,
tmp_hyper,
table="foo",
process_params={"default_database_version": "4"},
)
pt.frames_from_hyper(tmp_hyper)


@pytest.mark.parametrize(
"value,precision,scale",
[
Expand Down
16 changes: 15 additions & 1 deletion tests/test_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import pandas as pd
import pyarrow as pa
import pytest
import tableauhyperapi as tab_api

import pantab as pt

Expand Down Expand Up @@ -73,6 +72,7 @@ def test_append_mode_raises_ncolumns_mismatch(frame, tmp_hyper, table_name, comp

@pytest.mark.parametrize("container_t", [set, list, tuple])
def test_writer_creates_not_null_columns(tmp_hyper, container_t):
tab_api = pytest.importorskip("tableauhyperapi")
table_name = tab_api.TableName("test")
df = pd.DataFrame({"int32": [1, 2, 3]}, dtype="int32")
pt.frame_to_hyper(
Expand Down Expand Up @@ -100,6 +100,7 @@ def test_writing_to_non_nullable_column_without_nulls(tmp_hyper, container_t):
# With arrow as our backend we define everything as nullable, so it is up
# to the users to override this if they want
column_name = "int32"
tab_api = pytest.importorskip("tableauhyperapi")
table_name = tab_api.TableName("public", "table")
table = tab_api.TableDefinition(
table_name=table_name,
Expand Down Expand Up @@ -139,6 +140,7 @@ def test_writing_to_non_nullable_column_without_nulls(tmp_hyper, container_t):

def test_string_type_to_existing_varchar(frame, tmp_hyper, compat):
column_name = "string"
tab_api = pytest.importorskip("tableauhyperapi")
table_name = tab_api.TableName("public", "table")
table = tab_api.TableDefinition(
table_name=table_name,
Expand Down Expand Up @@ -223,6 +225,8 @@ def test_utc_bug(tmp_hyper):
}
)
pt.frame_to_hyper(frame, tmp_hyper, table="exp")

tab_api = pytest.importorskip("tableauhyperapi")
with tab_api.HyperProcess(
tab_api.Telemetry.DO_NOT_SEND_USAGE_DATA_TO_TABLEAU,
parameters={"log_config": ""},
Expand All @@ -249,6 +253,8 @@ def test_uint32_actually_writes_as_oid(tmp_hyper, frame):
table="test",
process_params={"default_database_version": "4"},
)

tab_api = pytest.importorskip("tableauhyperapi")
with tab_api.HyperProcess(
tab_api.Telemetry.DO_NOT_SEND_USAGE_DATA_TO_TABLEAU,
parameters={"log_config": ""},
Expand All @@ -272,6 +278,7 @@ def test_geo_and_json_columns_writes_proper_type(tmp_hyper, frame, container_t):
process_params={"default_database_version": "4"},
)

tab_api = pytest.importorskip("tableauhyperapi")
with tab_api.HyperProcess(
tab_api.Telemetry.DO_NOT_SEND_USAGE_DATA_TO_TABLEAU,
parameters={"log_config": ""},
Expand Down Expand Up @@ -322,6 +329,8 @@ def test_can_write_wkt_as_geo(tmp_hyper):
)

pt.frame_to_hyper(df, tmp_hyper, table="test", geo_columns=["geography"])

tab_api = pytest.importorskip("tableauhyperapi")
with tab_api.HyperProcess(
tab_api.Telemetry.DO_NOT_SEND_USAGE_DATA_TO_TABLEAU,
parameters={"log_config": ""},
Expand All @@ -346,6 +355,8 @@ def test_can_write_wkt_as_geo(tmp_hyper):

def test_can_write_chunked_frames(chunked_frame, tmp_hyper):
pt.frame_to_hyper(chunked_frame, tmp_hyper, table="test")

tab_api = pytest.importorskip("tableauhyperapi")
with tab_api.HyperProcess(
tab_api.Telemetry.DO_NOT_SEND_USAGE_DATA_TO_TABLEAU,
parameters={"log_config": ""},
Expand Down Expand Up @@ -378,6 +389,8 @@ def test_write_date_bug(tmp_hyper):
)

pt.frame_to_hyper(tbl, tmp_hyper, table="test")

tab_api = pytest.importorskip("tableauhyperapi")
with tab_api.HyperProcess(
tab_api.Telemetry.DO_NOT_SEND_USAGE_DATA_TO_TABLEAU,
parameters={"log_config": ""},
Expand All @@ -401,6 +414,7 @@ def test_eight_bit_int(tmp_hyper):

pt.frame_to_hyper(frame, tmp_hyper, table="test")

tab_api = pytest.importorskip("tableauhyperapi")
with tab_api.HyperProcess(
tab_api.Telemetry.DO_NOT_SEND_USAGE_DATA_TO_TABLEAU,
parameters={"log_config": ""},
Expand Down

0 comments on commit 9bb11de

Please sign in to comment.